Convert HTML to PDF
Prints your own HTML, CSS and images to PDF.
Send the page
Upload the page as index.html, with every file it uses:
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form files=@style.css \
-o invoice.pdf
Gotenberg keeps the names of the files and drops their folders. Link each file by its name alone: href="style.css", src="logo.png".
Send a ZIP
A page with many assets travels better as one ZIP. Upload it under archives, and Gotenberg unpacks it:
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form archives=@site.zip \
-o invoice.pdf
The entries lose their folders too, so the links stay bare names.
Wait for the page
Scripts run, unless the operator turned them off with --chromium-disable-javascript. Chromium prints the page as soon as it loads, though. A page that draws later with JavaScript, such as a chart or a table filled from an API, needs a wait. The examples upload this page, whose chart draws a second after it loads, to the HTML route; the fields work the same on every Chromium route:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Sales report</title>
</head>
<body>
<h1>Sales report</h1>
<div id="chart">Loading…</div>
<script>
// Stands in for a chart that draws once its data arrives.
setTimeout(() => {
const chart = document.getElementById("chart");
chart.textContent = "Revenue: 42,000 EUR";
chart.className = "ready";
window.chartReady = true;
window.status = "ready";
}, 1000);
</script>
</body>
</html>
Without a wait, the result reads "Loading…":
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
-o report.pdf
Each wait below gets "Revenue: 42,000 EUR" instead.
Wait for an element
Chromium waits until an element matching the CSS selector is visible:
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form waitForSelector=#chart.ready \
-o report.pdf
Mark the page ready from your own code, and wait for the mark. It's the most precise signal.
Wait for an expression
Chromium evaluates the JavaScript expression until it returns true:
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form 'waitForExpression=window.chartReady === true' \
-o report.pdf
An expression that throws, such as one that reads a property of undefined, answers 400.
Wait for the window status
Chromium waits until window.status equals the value:
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form waitForWindowStatus=ready \
-o report.pdf
Wait a fixed delay
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form waitDelay=2s \
-o report.pdf
A delay is the last resort. Too short, the chart is missing. Too long, every request pays for it.
Wait for the network
skipNetworkIdleEvent=false waits until the page stops making requests, for pages that fetch their content once they load:
curl \
--request POST http://localhost:3000/forms/chromium/convert/url \
--form url=https://example.com \
--form skipNetworkIdleEvent=false \
-o example.pdf
Order and limits
Chromium waits for the page to load, then for waitForWindowStatus, waitForExpression, waitForSelector and waitDelay, in that order. Every wait counts against --api-timeout, 30 seconds by default. A wait that never ends answers 503 Service Unavailable then, and names itself:
The selector '#never' (waitForSelector) did not become visible within the time limit. Increase it with --api-timeout (API_TIMEOUT), or check the selector.
waitDelay must be shorter than --api-timeout. A longer one answers 400 right away, and names the flag:
Invalid form data: form field 'waitDelay' is invalid (got '60s', must be shorter than the API timeout of 30s: raise --api-timeout (API_TIMEOUT) or lower the delay)
Set the page size and margins
Pick a paper size
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form files=@style.css \
--form paperSize=A4 \
-o invoice.pdf
paperSize takes A3, A4, A5, B4, B5, Letter, Legal or Tabloid. Chromium prints on Letter without it.
Turn it sideways
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form files=@style.css \
--form paperSize=A4 \
--form landscape=true \
-o invoice.pdf
Set a custom size
paperWidth and paperHeight take a length: a number of inches, or a number with a unit, mm, cm, in, pt, px or pc:
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form files=@style.css \
--form paperWidth=210mm \
--form paperHeight=99mm \
-o invoice.pdf
Next to paperSize, each overrides one side.
Set the margins
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form files=@style.css \
--form marginTop=2cm \
--form marginBottom=2cm \
--form marginLeft=1.5cm \
--form marginRight=1.5cm \
-o invoice.pdf
Each margin is 0.39 inches by default. A negative margin, paperWidth or paperHeight answers 400. Headers and footers print inside the top and bottom ones.
Let the CSS decide
preferCssPageSize takes the size from the page's CSS @page rule. This page, sent to the HTML route, sets size: A5:
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form files=@print.css \
--form preferCssPageSize=true \
-o handbook.pdf
Without it, Chromium takes the size from the fields, but keeps the orientation of the @page size: a portrait rule keeps the PDF portrait, over landscape.
Fit the content on one page
singlePage prints the whole page on one page, as tall as the content, such as a receipt:
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form files=@style.css \
--form singlePage=true \
-o invoice.pdf
scale shrinks or enlarges the content instead, from 0.1 to 2. A scale outside that range answers 400.
Add a header and a footer
Print a title, page numbers or a date on every page. Write the footer as its own small HTML file:
<!doctype html>
<html>
<head>
<style>
.footer {
margin: 0 2cm 0 auto;
font-family: sans-serif;
font-size: 9px;
}
</style>
</head>
<body>
<p class="footer">
Page <span class="pageNumber"></span> of <span class="totalPages"></span>
</p>
</body>
</html>
Upload it as footer.html:
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form files=@style.css \
--form files=@footer.html \
-o invoice.pdf
header.html works the same way, at the top of each page:
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form files=@style.css \
--form files=@header.html \
--form files=@footer.html \
-o invoice.pdf
Chromium fills in the elements with these classes:
| Class | Value |
|---|---|
pageNumber | The current page. |
totalPages | The page count. |
title | The page's <title>. |
date | The print date, as 11/1/26, 5:55 PM. |
url | The page's address, meaningful on the URL route. |
Style them
Chromium renders the header and footer apart from the page, with rules of their own:
- Set a font size. Without one, the text is about one point tall.
- Style them in a
<style>tag. They don't load stylesheets. - Embed images as data URIs, such as
src="data:image/png;base64,…". They don't load files. - Give each its own class names. Chromium renders both in one document, so a rule in the footer reaches the header.
Make room
The header and footer print inside the top and bottom margins, 0.39 inches by default. Widen them for a taller header:
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form files=@style.css \
--form files=@header.html \
--form files=@footer.html \
--form marginTop=1in \
--form marginBottom=1in \
-o invoice.pdf
On any PDF, the stamp-text stage writes the same text on every page, but it can't number them.
Style the print
Chromium prints with the page's print styles. This stylesheet hides the navigation and starts each chapter on a new page:
@page {
size: A5;
margin: 15mm;
}
body {
font-family: sans-serif;
}
h2 {
break-before: page;
}
@media print {
nav {
display: none;
}
}
Sent with its page to the HTML route:
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form files=@print.css \
-o handbook.pdf
The PDF has two pages and no navigation. emulatedMediaType=screen prints the page as a screen shows it instead, navigation included. The @page size waits for preferCssPageSize: see Let the CSS decide.
Print backgrounds
Chromium prints the backgrounds of elements, such as table fills and colored boxes, whatever the field. printBackground decides the page's own background, the one set on html and body: Chromium leaves it out by default, as a browser's print dialog does. Turn it on:
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form files=@style.css \
--form printBackground=true \
-o invoice.pdf
Add bookmarks and tags
Bookmarks
nativeBookmarks turns the page's headings into bookmarks, each nested under the heading above it:
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form files=@style.css \
--form nativeBookmarks=true \
-o invoice.pdf
The set-bookmarks stage writes bookmarks of your own instead.
Tags
A tagged PDF carries its structure, so screen readers can follow it: headings, paragraphs, lists, tables, figures. nativeTaggedPdf tags the PDF from the HTML:
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form files=@style.css \
--form nativeTaggedPdf=true \
-o invoice.pdf
Write semantic HTML, with headings, lists, tables and alt text on images, and the tags follow it. The remediate stage goes on to PDF/UA.
Add stages
Stages run on the PDF Chromium prints, in order, before Gotenberg answers. List them in stages, each naming its operation in op, with its options beside it:
curl \
--request POST http://localhost:3000/forms/chromium/convert/html \
--form files=@index.html \
--form files=@style.css \
--form 'stages=[
{ "op": "stamp-text", "text": "DRAFT", "x": 72, "y": 72 },
{ "op": "optimize" }
]' \
-o invoice.pdf
Every stage works here: merge, stamp, redact, sign, make PDF/A, and so on. A stage that names a file, such as an image to stamp, takes it from the files you upload. Stages lists them all.