Skip to main content
Version: 9.x

Convert HTML to PDF

Prints your own HTML, CSS and images to PDF.

POST/forms/chromium/convert/html
filesfile[]required

index.html, and the files it loads: style sheets, images, fonts, scripts. Link each by its name alone, such as style.css.

stagesjson

Stages to run on the PDF, in order, such as [{"op":"merge"}]. Upload the files they name under files.

archivesfile[]

ZIP files Gotenberg unpacks: each entry counts as a file uploaded under files.

URLs Gotenberg downloads first, as if you had uploaded the files. Each entry takes a url, and optionally extraHttpHeaders.

waitDelayduration

Waits this long once the page loads, before printing: 2s, 500ms. It must be shorter than --api-timeout, or the request answers 400.

Waits until an element matching this CSS selector is visible, such as #chart.ready.

Waits until this JavaScript expression returns true, such as window.ready === true.

Waits until window.status equals this value.

Prints without waiting for the network to go idle. false waits, for pages that fetch content after they load.

Default: true

Prints without waiting for the network to go almost idle. false waits.

Default: true

Answers 409 when the page itself answers one of these statuses. 499 stands for 400 to 499, 599 for 500 to 599, and so on for each hundred.

Default: [499,599]

Answers 409 when a resource the page loads, such as an image or a script, answers one of these statuses. Ranges work as for failOnHttpStatusCodes.

Domains, subdomains included, that failOnResourceHttpStatusCodes leaves out: ["sentry-cdn.com"].

Answers 409 when a resource's load fails on the network: a refused or dropped connection, a host that doesn't resolve, a blocked request. A timeout or a certificate error doesn't count.

Default: false

Answers 409 when the page throws a JavaScript exception.

Default: false

Answers 409 when the page writes one of these console types: log, debug, info, warning, error or assert.

Cookies to set before loading: items with a name, a value and a domain, and optionally a path, secure, httpOnly and sameSite (Strict, Lax or None).

Headers Chromium sends with the requests the page makes: {"X-Token": "abc"}. End a value with ;scope=<regex> to send it only to the URLs the regex matches. At most 64 headers.

userAgentstring

Replaces Chromium's User-Agent header.

The CSS media type the page sees: screen or print.

CSS media features the page sees: {"prefers-color-scheme": "dark"}.

Drops the default white background, for a transparent PDF or image. A PDF needs printBackground too.

Default: false

paperSizestring

A3, A4, A5, B4, B5, Letter, Legal or Tabloid, in any case. Sets the width and the height.

Default: Letter

Overrides the width paperSize sets. A length is a number in inches, or a number with a unit: 210mm, 21cm, 595pt, 794px, 50pc. A negative length answers 400.

Default: 8.5in

Overrides the height paperSize sets. A negative length answers 400.

Default: 11in

marginToplength

The top margin. The header prints in it. A negative margin answers 400, as for the other margins.

Default: 0.39in

The bottom margin. The footer prints in it.

Default: 0.39in

The left margin.

Default: 0.39in

The right margin.

Default: 0.39in

landscapeboolean

Turns the paper sideways.

Default: false

Uses the size the page's CSS @page rule sets, over the paper size.

Default: false

scalenumber

Scales the rendering, from 0.1 to 2. A value outside that range answers 400.

Default: 1

Prints the page's own background, set on html and body. Chromium prints the backgrounds of elements, such as table fills and colored boxes, whatever this field says.

Default: false

singlePageboolean

Prints the whole page on one page, as tall as the content.

Default: false

Pages to print, in Chromium's syntax: 1-5, 8, 11-13. A page past the end answers 400. The select stage picks pages after printing, and takes odd, even and last too.

Upload it under files: Chromium prints it at the top of each page. Elements with the class date, title, url, pageNumber or totalPages receive that value. It shares one document with the footer, so give each its own class names.

The same, at the bottom of each page.

Builds bookmarks from the headings. Implies nativeTaggedPdf.

Default: false

Writes a tagged PDF, whose structure screen readers follow.

Default: false

The PDF. A stage that makes several files, such as split, answers a ZIP.

The route also takes the headers. An error answers a status and a message: see Errors.

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:

index.html
<!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.

Print a title, page numbers or a date on every page. Write the footer as its own small HTML file:

footer.html
<!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:

ClassValue
pageNumberThe current page.
totalPagesThe page count.
titleThe page's <title>.
dateThe print date, as 11/1/26, 5:55 PM.
urlThe 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:

print.css
@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.

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.