Skip to content
CastPDF

Generate PDFs in Node.js without running Chrome

To generate a PDF in Node.js, send your HTML or your template data to CastPDF with the built-in fetch and write the response to a file. There is no browser to install, patch or restart. It is the same PDF generation API every language uses, and it works in Node 20 or later.

  • Updated October 2026
  • 100 free PDFs a month

Your options in Node.js

Node has three common ways to make a PDF. Each one is a good fit for some jobs, so pick by what your documents look like and who has to run the servers.

Ways to make a PDF in Node.js
ApproachGood forWhat you take on
Puppeteer or PlaywrightFull control over a headless Chrome you host yourselfA browser binary in every container, memory to watch, crashes and blank pages to debug under load
PDFKit or pdfmakeSimple layouts drawn in code, no HTML at allPositioning every line by hand; tables that run over pages need your own logic
jsPDFSmall PDFs made in the browserLimited CSS support; not built for long, multi-page business documents
An API such as CastPDFHTML and CSS you already know, rendered by Chromium with print features addedA network call per PDF and a monthly plan above the free allowance

If you already run Puppeteer happily and your documents fit on one page, you may not need anything else. Teams usually move to an API when the invoices get long: table headers stop repeating, rows split across pages, and the Chrome processes need their own on-call rota.

Your first PDF in about 15 lines

Create a free test key in the dashboard (it starts with cpdf_test_) and keep it in an environment variable. Then save this file as first.mjs and run node first.mjs. Test PDFs are free, never count towards your plan and carry a watermark.

first.mjs (Node 20+)
import { writeFile } from 'node:fs/promises';

const res = await fetch('https://api.castpdf.com/v1/pdf', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CASTPDF_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    html: '<h1>Hello {{ name }}</h1><p>Your order ships today.</p>',
    data: { name: 'Ada' },
  }),
});
if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message}`);
}
await writeFile('first.pdf', Buffer.from(await res.arrayBuffer()));
console.log('pages:', res.headers.get('x-pages'));

Because the request has a data object, the HTML is treated as a Liquid template and {{ name }} is filled in before rendering. Leave data out and the HTML is used exactly as you send it. The response body is the PDF itself, and the x-pages and x-document-id headers describe it.

Fill a saved template with JSON

For documents you send again and again, keep the design in a template and send only the data. Pick a starter in the dashboard (or create one with POST /v1/templates and a live key), copy its ID, and send the values. Ask for "response": "url" when you want a private download link instead of the file, for example to email it or store it in your database.

invoice.mjs
const res = await fetch('https://api.castpdf.com/v1/pdf', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.CASTPDF_API_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'invoice-INV-2026-0142',
  },
  body: JSON.stringify({
    template_id: process.env.INVOICE_TEMPLATE_ID,
    data: {
      invoice: { number: 'INV-2026-0142', issued: '2026-10-03', due: '2026-11-02' },
      customer: { name: 'Northwind Traders' },
      items: [
        { description: 'Design sprint', quantity: 3, unit_price: 950 },
        { description: 'Hosting, October', quantity: 1, unit_price: 49 },
      ],
    },
    filename: 'INV-2026-0142',
    response: 'url',
  }),
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
console.log(body.url, 'expires', body.expires_at);

The data object replaces the template’s sample data as a whole, so send every field the template uses. The fields above are shortened; the invoice template lists them all.

Send the PDF to a browser from your server

Never call CastPDF from code that runs in the browser: anyone could read your key there. Call it from your server and pass the bytes on. This plain node:http server answers /receipt with a PDF the browser can open or save. The Express page shows the same in a route, and Next.js in a Route Handler.

server.mjs
import { createServer } from 'node:http';

createServer(async (req, res) => {
  if (req.url !== '/receipt') {
    res.writeHead(404).end();
    return;
  }
  const pdf = await fetch('https://api.castpdf.com/v1/pdf', {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.CASTPDF_API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ template_id: process.env.RECEIPT_TEMPLATE_ID, filename: 'receipt' }),
    signal: AbortSignal.timeout(60_000),
  });
  if (!pdf.ok) {
    res.writeHead(502, { 'Content-Type': 'text/plain' }).end('Could not make the PDF');
    return;
  }
  res.writeHead(200, { 'Content-Type': 'application/pdf', 'Content-Disposition': 'inline; filename="receipt.pdf"' });
  res.end(Buffer.from(await pdf.arrayBuffer()));
}).listen(3000);

Before you go live

  • Keep the key in an environment variable or your secrets manager, and switch from the cpdf_test_ key to a cpdf_live_ key when you launch. Nothing else changes.
  • Give each request at least 60 seconds with AbortSignal.timeout(60_000). A render may take up to 30 seconds, plus queue time when the service is busy.
  • Send an Idempotency-Key that names the document in your system, such as the invoice number. A retried request then returns the first PDF instead of making, and billing, a second one.
  • Retry 429 after the Retry-After seconds and 503 with exponential backoff. Other 4xx answers describe a problem with the request, so retrying gives the same result.
  • Keep documents under 50 pages and 40 MB. Large images are the usual reason a PDF grows.

Errors you might see

Every error has the same JSON shape with a code, a readable message and a docs_url. These are the ones Node developers meet first. The errors page lists every code.

Common API errors and their fixes
CodeWhat it meansWhat to do
invalid_api_key (401)The key is missing, mistyped or revokedCheck process.env.CASTPDF_API_KEY is set where the code runs
template_render_error (422)A Liquid tag or filter failedRead details.line and details.column, then fix the template or the data
rate_limited (429)Too many requests this minute (a test key allows 20)Wait for Retry-After seconds, then retry
plan_limit_reached (402)The Free plan’s live PDFs are used up for the monthUpgrade, or keep building with the test key
render_timeout (504)Rendering took longer than 30 secondsLook for slow external images or fonts, or split the document

Next steps: the quickstart walks through the dashboard side, and the API reference documents every field. Using Python too? See the Python page.

FAQ

Common questions

Do I need to install Puppeteer or Chromium to make PDFs in Node.js?

No. CastPDF runs Chromium on its own servers. Your Node code sends one HTTPS request with the built-in fetch and gets the PDF back, so there is no browser in your container and no memory to watch.

Is there an npm package for CastPDF?

Not yet, and you do not need one. The API is a single POST request with a JSON body, so the fetch built into Node 20 and later is all it takes. The examples on this page run as they are.

Can I use TypeScript?

Yes. The same fetch code works in TypeScript. Type the response JSON yourself, for example with an interface for the url and expires_at fields, or with the error shape of code, message and docs_url.

How do I get page numbers and repeating table headers?

Templates render in print mode by default, which repeats a table’s thead on every new page and supports page counters in the page margins. For raw HTML, send "mode": "print". The print CSS guide in the docs has copy-ready rules.

Can I run this in a serverless function?

Yes, and it is a good fit, because the function does not have to bundle a browser. Keep the function timeout above 60 seconds for long documents. The AWS Lambda, Supabase and Firebase pages show working functions.

How much does it cost to generate PDFs from Node?

The language does not change the price. Test PDFs are free and unlimited. Live PDFs count one per document, whatever its page count, and the Free plan includes a monthly allowance with no card needed.

Make your first PDF in 5 minutes

Pick a template, add your details and download your PDF. You get 100 free PDFs every month, and you don’t need a card.