Convert PowerPoint to PDF in Node.js — Without Office on Your Server
Search npm for “pptx to pdf” and you'll find a handful of wrapper packages — and almost every one of them starts its README with the same requirement: install a full office suite on the machine first, sometimes plus image tools. That's fine on a laptop, and a headache in a Docker image, on a PaaS or in a serverless function.
With Apdf, the conversion happens behind an API. Your Node.js code sends the deck's URL,
waits for the job and downloads the PDF — using nothing but the fetch
that ships with Node 18 and newer.
convertToPdf() helper, and an Express route that
serves any sales deck as a PDF — converted on the first request, cached after that:
GET /decks/kickoff-globex-2026.pdf → 2.2 s on the first request, 2 ms from the cache
Convert a deck
The convert endpoint takes the deck's URL and answers right away with a
job_id; the conversion runs in the background. The helper polls the
job until the PDF is ready. Save it as convert.mjs:
import { writeFile } from 'node:fs/promises';
const API_TOKEN = 'YOUR_API_TOKEN';
const BASE_URL = 'https://apdf.io/api';
const headers = { Authorization: `Bearer ${API_TOKEN}`, Accept: 'application/json' };
async function post(path, body) {
const response = await fetch(`${BASE_URL}${path}`, { method: 'POST', headers, body: new URLSearchParams(body) });
if (!response.ok) {
throw new Error(`${path} failed with ${response.status}: ${await response.text()}`);
}
return response.json();
}
// Start the conversion, then poll the job until the PDF is ready.
export async function convertToPdf(documentUrl, timeoutMs = 300_000) {
const { job_id } = await post('/pdf/file/convert', { file: documentUrl });
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const job = await post('/job/status/check', { id: job_id });
if (job.status === 'successful') return job.result;
if (job.status === 'failed') throw new Error(`Conversion failed: ${JSON.stringify(job.result)}`);
await new Promise((resolve) => setTimeout(resolve, 2000));
}
throw new Error(`Job ${job_id} did not finish in time`);
}
// Download the finished PDF (the link expires after one hour).
export async function downloadPdf(result) {
const response = await fetch(result.file);
if (!response.ok) throw new Error(`Download failed with ${response.status}`);
return Buffer.from(await response.arrayBuffer());
}
if (import.meta.url === `file://${process.argv[1]}`) {
const result = await convertToPdf('https://files.northlight.example/decks/proposal-acme-q4-2026.pptx');
await writeFile('proposal-acme-q4-2026.pdf', await downloadPdf(result));
console.log(`Saved proposal-acme-q4-2026.pdf: ${result.pages} pages, ${result.size} bytes`);
}
Run node convert.mjs and the five-slide proposal comes back as a
five-page PDF, still in its widescreen format. This is the finished job the helper reads:
{
"id": "01M4D7REN6R9SJA4Z658R0RCDQ",
"created": "2026-10-08T07:48:31.000000Z",
"status": "successful",
"result": {
"file": "https://apdf-files.s3.eu-central-1.amazonaws.com/5a9656b46ac74acf7ebeb.pdf",
"size": 42991,
"pages": 5,
"expiration": "2026-10-08T08:48:32.440485Z"
}
}
getSignedUrl from @aws-sdk/s3-request-presigner)
that stays valid for a few minutes.
Serve every deck as a PDF
Sales wants a PDF link for every deck in the shared folder — without anyone exporting them by hand. An Express route does it on demand: the first request converts the deck, later requests are served from a disk cache. The cache matters, because the PDF link Apdf returns expires after one hour:
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import express from 'express';
import { convertToPdf, downloadPdf } from './convert.mjs';
const DECKS_URL = 'https://files.northlight.example/decks';
const CACHE_DIR = './pdf-cache';
const app = express();
await mkdir(CACHE_DIR, { recursive: true });
// GET /decks/proposal-acme-q4-2026.pdf → the deck as PDF, converted on first request.
app.get('/decks/:name.pdf', async (req, res) => {
const name = req.params.name.replace(/[^a-z0-9-]/gi, '');
const cached = `${CACHE_DIR}/${name}.pdf`;
try {
const pdf = await readFile(cached).catch(async () => {
const result = await convertToPdf(`${DECKS_URL}/${name}.pptx`);
const converted = await downloadPdf(result);
await writeFile(cached, converted);
return converted;
});
res.type('application/pdf').send(pdf);
} catch (error) {
console.error(error);
res.status(502).send('The deck could not be converted.');
}
});
app.listen(3000, () => console.log('Listening on http://localhost:3000'));
Request the same deck twice and the difference is obvious:
$ curl -o kickoff.pdf -w "%{http_code} %{time_total}s\n" http://localhost:3000/decks/kickoff-globex-2026.pdf
200 2.211315s
$ curl -o kickoff.pdf -w "%{http_code} %{time_total}s\n" http://localhost:3000/decks/kickoff-globex-2026.pdf
200 0.002144s
A deck that doesn't exist answers with a 502, and the server log shows the reason the job failed:
Error: Conversion failed: {"errors":{"file":"404 Not Found"},"message":"The file could not be downloaded."}
pdf-cache — or version your file names —
so the next request converts the new version.
Use it for Word and Excel too
convertToPdf() isn't limited to slides. The same endpoint converts
.ppt decks, Word documents (.docx,
.doc), Excel workbooks (.xlsx,
.xls), OpenDocument files and RTF — pass any of them and
every slide or page becomes a PDF page.
Where to go from here
The deck is a PDF now — so you can find out who actually reads it.
Related tutorials
Convert Word to PDF in Python — Without Microsoft Word on Your Server
docx2pdf needs Microsoft Word, and an office suite on the server needs babysitting. Send the .docx URL to the Apdf API instead and download the PDF — one script for a single document, and a webhook receiver that converts a whole batch of client reports without polling.
Extract Text from Scanned Invoices for Automated Data Entry
Automate data entry from scanned invoices and receipts using Node.js and the Apdf OCR Read API. Extract vendor names, invoice numbers, and totals from image-based PDFs and feed them directly into your accounting system.
Compress Large PDFs for Email Attachments with Node.js
Reduce PDF file sizes before sending email attachments using Node.js and the Apdf Compress API. Avoid bounced emails, improve deliverability, and save bandwidth with automatic compression that can shrink files by 50-90%.
Your code made the PDF.
Then it went dark.
Opened, read, signed or ignored — you never see any of it. Send the PDFs you generate as personal links, and every open, page read, comment and signature comes back to your software as a webhook. Need it signed? Same link. Same account, same API key, one more call.
API · Webhook · MCP