Skip to content
CastPDF

Send HTML, get a print-ready PDF

An HTML to PDF API takes the HTML and CSS your app already makes and returns a finished PDF from one HTTPS request. CastPDF renders it in Chromium, repeats table headers, keeps rows whole and prints page numbers. It is the HTML route of our PDF generation API, with nothing to install.

  • Updated October 2026
  • 100 free PDFs a month

Send HTML in one request

Your app already knows how to build HTML. It does it for every web page it serves. An HTML to PDF API lets you reuse that skill for paper: you post the markup and a stylesheet, and the response is the PDF file itself.

The request body needs one field, html. Add css for styles, and page options such as format and margins if the defaults do not suit you. Here is a short packing list sent from the command line:

curl: HTML and CSS to a Letter-size PDF
curl https://api.castpdf.com/v1/pdf \
  -H "Authorization: Bearer $CASTPDF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<h1>Packing list</h1><table><thead><tr><th>SKU</th><th>Item</th><th>Qty</th></tr></thead><tbody><tr><td>LMP-204</td><td>Brass desk lamp</td><td>2</td></tr></tbody></table>",
    "css": "body { font-family: Noto Sans, sans-serif; } th { text-align: left; }",
    "format": "Letter",
    "margins": "18mm"
  }' \
  --output packing-list.pdf

Real documents are built from data. Send a data object with the HTML and the HTML becomes a Liquid template, so loops and filters run before the page is printed. The next two examples make a multi-page sales table with a page counter in the footer.

Node.js 20+: HTML, CSS and data, in print mode
import { writeFile } from 'node:fs/promises';

const html = `<h1>September sales</h1>
<table>
  <thead><tr><th>Date</th><th>Store</th><th>Revenue</th></tr></thead>
  <tbody>
    {% for row in rows %}<tr><td>{{ row.date }}</td><td>{{ row.store }}</td><td>{{ row.revenue | money: "EUR", "pt-PT" }}</td></tr>{% endfor %}
  </tbody>
</table>`;

const css = '@page { @bottom-right { content: "Page " counter(page) " of " counter(pages); font-size: 8pt; } } th { text-align: left; }';

const rows = [
  { date: '2026-09-01', store: 'Lisbon Baixa', revenue: 18420.5 },
  { date: '2026-09-01', store: 'Porto Ribeira', revenue: 9310 },
];

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, css, data: { rows }, mode: 'print', format: 'A4', margins: '20mm 18mm 24mm' }),
  signal: AbortSignal.timeout(60_000),
});
if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message}`);
}
await writeFile('september-sales.pdf', Buffer.from(await res.arrayBuffer()));
console.log('pages:', res.headers.get('x-pages'));
Python: the same report with requests
import os
import requests

html = """
<h1>September sales</h1>
<table>
  <thead><tr><th>Date</th><th>Store</th><th>Revenue</th></tr></thead>
  <tbody>
    {% for row in rows %}<tr><td>{{ row.date }}</td><td>{{ row.store }}</td><td>{{ row.revenue | money: "EUR", "pt-PT" }}</td></tr>{% endfor %}
  </tbody>
</table>
"""
css = '@page { @bottom-right { content: "Page " counter(page) " of " counter(pages); font-size: 8pt; } }'
rows = [
    {"date": "2026-09-01", "store": "Lisbon Baixa", "revenue": 18420.5},
    {"date": "2026-09-01", "store": "Porto Ribeira", "revenue": 9310},
]

res = requests.post(
    "https://api.castpdf.com/v1/pdf",
    headers={"Authorization": f"Bearer {os.environ['CASTPDF_API_KEY']}"},
    json={"html": html, "css": css, "data": {"rows": rows}, "mode": "print", "format": "A4", "margins": "20mm 18mm 24mm"},
    timeout=60,
)
res.raise_for_status()
with open("september-sales.pdf", "wb") as f:
    f.write(res.content)
print("pages:", res.headers["x-pages"])

By default the body of the response is the PDF. The x-pages header tells you how many pages it has, and x-document-id lets you look the document up later. Prefer a link? Add "response": "url" and you get JSON with a signed download address that works without your key until it expires.

There is no package to install. Any HTTP client works, and the Node.js and Python pages add error handling and retries for production.

Long documents that behave like paper

Page one is easy. The trouble starts when a table runs past the bottom edge. A row gets sliced in half, the column names vanish on page two, and nobody can tell page 3 from page 7. That is the most common complaint about HTML to PDF tools, and it is what print mode is built to fix.

  • Headers that repeat. Put your column names in a <thead> and they appear again at the top of every continued page. The repeating table header guide shows the markup.
  • Rows that stay whole. Table rows and images avoid splitting across pages by default. Add the class keep-together to any block that must stay on one page, such as a totals box or a signature.
  • Page numbers in the margin. "Page 2 of 6" comes from standard CSS margin boxes and the page and pages counters. See page numbers in HTML to PDF.
  • Breaks where you want them. break-before: page starts a chapter on a fresh page, and the older page-break-* rules work too. The page breaks guide covers the edge cases.
  • Running headers. A chapter title can follow the reader across pages with string-set, a feature that browsers alone do not offer.

Print mode also protects you from silent damage. If some content would be cut off, the request fails with content_lost and tells you why. You never receive a PDF with a missing paragraph that nobody notices until a customer does.

Coming from Puppeteer’s headerTemplate and footerTemplate? The Puppeteer header and footer guide shows how the same headers look as print CSS.

Fast mode or print mode

Every request runs in one of two modes. HTML defaults to fast mode, because many HTML documents are short. Switch to print mode with "mode": "print" when the document needs the features above.

What each mode does
Fast modePrint mode
How pages are madeChromium’s own print enginePaged.js lays out the pages first, then Chromium prints them
Default forRequests that send htmlRequests that send a template_id
Page size, margins and break rulesYesYes
Repeating table headersChromium’s built-in behaviourCastPDF’s own handler, tested on long invoices
Page counters and margin boxesUse print mode for theseYes
Running headers and page numbers in a table of contentsNoYes
Stops a PDF with cut-off contentNoYes, with content_lost
SpeedQuicker, most of all on long documentsA little slower, because of the layout step

A simple rule works well. Use fast mode for one-page letters, labels and receipts. Use print mode for anything with a long table, a footer with page numbers or chapters.

Modern CSS works, because it is Chromium

The renderer is the same engine as the Chrome browser. Flexbox, grid, custom properties, calc(), web fonts and SVG all behave as they do on screen. If your layout looks right in Chrome’s print preview, it will look very close to that in the PDF.

Backgrounds and colours print exactly as written, with no extra setting. JavaScript in your page runs before printing, within the same time limit, so a small script that formats dates or draws an SVG still works. An error in your script does not stop the render.

Utility CSS is fine too. If you use a framework such as Tailwind, send the compiled stylesheet in css or link it from a public address. Keep the file lean: the whole document, HTML and CSS together, may be up to 5 MB after any Liquid has run.

Page size, margins and orientation

Without any options you get A4 portrait pages with 20mm margins. These request fields change that for one document:

Page fields of POST /v1/pdf
FieldValuesNotes
format"A4", "A5", "Letter", "Legal", or { "width": "100mm", "height": "150mm" }A custom size takes lengths such as millimetres or inches.
orientation"portrait" or "landscape"Turns a named format on its side. A custom size in landscape puts its wider side first.
marginsOne to four lengths, such as "20mm" or "1in 0.75in"Works like the CSS margin shorthand, in mm, cm, in, px or pt.
mode"fast" or "print"Fast is the default for HTML.
filenameUp to 100 charactersThe name the browser suggests when someone downloads the file.

These options become an @page rule placed before your CSS. An @page size or margin written in your own CSS wins. Keep one in your stylesheet only if the document must always print the same way, whatever the request says.

Print CSS: a fixed page, a footer and a cover without a number
@page {
  size: A4;
  margin: 22mm 18mm 26mm;
  @bottom-center { content: "Page " counter(page) " of " counter(pages); font-size: 8pt; color: #64748b; }
}
@page :first {
  @bottom-center { content: none; }
}
.cover { break-after: page; }
.totals { break-inside: avoid; }

Margin boxes live inside the page margin, so give the bottom margin enough room for the footer text. The print CSS reference lists every margin box and counter.

Fonts and images

Missing fonts are the second classic PDF bug. The preview shows your brand font, the server falls back to Times, and the invoice looks like a ransom note. CastPDF avoids most of this, because common font families are already installed on the renderer.

  • Installed and ready: Noto Sans and Noto Serif with the Noto families for other scripts, Noto CJK for Chinese, Japanese and Korean, Noto Color Emoji, Liberation and DejaVu.
  • Any other font: link a stylesheet from a public font service, or write an @font-face rule with a public https address. The renderer waits for fonts to load before it prints.
  • Fully offline: embed the font file as a data: URL. It needs no network request, but it does make the HTML bigger.
  • Images: use public http or https addresses, or data: URLs. Each document may load up to 300 resources and 20 MB in total.
  • Never reachable: localhost, private network addresses and files behind a login. The renderer refuses them on purpose, to keep your network safe.

A refused or failed image does not fail the whole document. It is simply missing, and the response counts it in the x-blocked-resources header. A plain 404 from your own server is not counted there, so open your test PDFs and look. The fonts in HTML to PDF guide goes deeper on loading and fallbacks.

Free test PDFs and the Free plan

Test keys start with cpdf_test_. They make unlimited PDFs for free, with a diagonal "CASTPDF TEST" watermark on each page, at up to 20 requests a minute. Use them while you build and in your automated tests.

Live keys start with cpdf_live_ and make clean PDFs. The Free plan includes 100 live PDFs a month with no card, with a small "Made with CastPDF" line at the bottom of each page. Starter costs $19 a month for 2,500 PDFs, and Growth costs $49 for 10,000.

You pay per finished document, never per page. A 40-page report counts the same as a one-page receipt. Failed requests are never counted. The pricing page lists every plan.

HTML or a stored template: which to pick

The same endpoint takes either raw HTML or the ID of a template saved in CastPDF. Both use the same renderer and the same print features. The difference is where the design lives and who can change it.

Raw HTML compared with a stored template
Send `html`Send a `template_id`
Where the layout livesIn your code, next to the rest of your appIn CastPDF, with a live preview editor
Who can change itDevelopers, with a deployAnyone with dashboard access, without a deploy
DataOptional: add data to run Liquid on the HTMLRequired in practice: the template fills itself from your JSON
HistoryYour own version controlVersions saved on every change, the latest 50 kept, and one can be pinned
Default modeFastPrint
Good forPages your app already renders, one-off exports, quick testsInvoices, certificates and other documents you make every day

Many teams start with HTML and move the busiest document into a template later. The template editor has a Simple mode form for the sample data, so a colleague can check a change without touching code. The design itself stays HTML and CSS. The templates documentation covers versions and pinning.

Migrating from wkhtmltopdf or Puppeteer

Most people who look for an HTML to PDF API already make PDFs somehow. The move is usually small, because your HTML stays the same. What changes is who runs the browser.

From wkhtmltopdf

The wkhtmltopdf project was archived in January 2023, and its engine is an old version of WebKit. Layouts written around its quirks often used floats and tables instead of flexbox or grid. Those layouts still render in Chromium, so you can migrate first and modernise later.

  • Page size and orientation flags become the format and orientation fields.
  • The four margin flags become one margins string, such as "15mm 12mm".
  • Header and footer options become CSS margin boxes, rendered in print mode.
  • Fonts installed on your old server may not exist here. Check the installed list above, or load them from a public address.

Our wkhtmltopdf alternatives page compares the self-hosted and hosted options side by side.

From Puppeteer

Puppeteer gives you full control of a browser. You also own its memory use, its crashes and its fonts. When you call CastPDF instead, the options of page.pdf() map onto request fields:

Puppeteer options and their CastPDF fields
PuppeteerCastPDF request
page.setContent(html)"html": "...", plus "css" if you keep styles apart
format: "A4""format": "A4"
landscape: true"orientation": "landscape"
margin: { top, right, bottom, left }"margins": "top right bottom left" as one string
printBackground: trueNot needed: backgrounds always print
headerTemplate and footerTemplate@page margin boxes with "mode": "print"

One difference matters. CastPDF takes HTML, not a web address, so there is no field for "open this URL". If you used page.goto() on your own pages, render that HTML on your server and send it. The CastPDF vs Puppeteer page weighs up when self-hosting is still the better choice.

Before you go live

  • Keep the key on your server. Never put an API key in browser or mobile code. Your backend calls CastPDF and hands the PDF or the link to the user.
  • Allow enough time. A render may take up to 30 seconds, so set your HTTP client timeout to at least 60 seconds.
  • Know the ceilings. A document can have up to 50 pages and 40 MB, and a request body up to 5 MB.
  • Retry the right errors. Wait for the Retry-After header on 429, back off on 503, and retry network errors with the same Idempotency-Key.
  • Read the error code. Every error returns a code, a message and a link to its documentation, so your logs say what went wrong.

The quickstart gets a first PDF out in a few minutes, and the API reference lists every field and error.

FAQ

Common questions

Does the HTML to PDF API support CSS grid and flexbox?

Yes. Pages are rendered by Chromium, so grid, flexbox, custom properties and web fonts work as they do in Chrome. Older engines such as wkhtmltopdf lack most of these, which is a common reason to switch.

Can I turn a web page address into a PDF?

Not directly. CastPDF takes HTML in the request body rather than a URL to visit. Render or fetch the HTML on your own server, then send it, together with any CSS it needs.

Does JavaScript run before the PDF is printed?

Yes. Scripts in your HTML run before printing, within the render time limit. A script error does not stop the document. For anything heavy, do the work on your server and send the finished HTML.

Why is an image missing from my PDF?

Usually its address is not public. The renderer refuses localhost, private networks and pages behind a login, and reports refused requests in the x-blocked-resources header. Use a public https address or embed the image as a data URL.

When should I switch from fast mode to print mode?

Switch when a document has a long table, page numbers, running headers or a table of contents. Print mode lays out every page before printing, which makes those features work. Fast mode is quicker for short, simple pages.

Do I need to escape my HTML before sending it?

Only as JSON. Build the body with your language’s JSON encoder, such as JSON.stringify in JavaScript or the json argument in Python requests. The encoder escapes quotes and line breaks, and the HTML arrives unchanged.

How large can my HTML be?

The request body may be up to 5 MB. The HTML and CSS together may be up to 5 MB after Liquid has run. Large embedded images are the usual reason a document grows too big.

Can I test the API without paying?

Yes. Test keys make unlimited watermarked PDFs at no cost on every plan. The Free plan adds 100 clean live PDFs a month, and no card is needed to sign up.

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.