Convert a URL to PDF
Prints any page Chromium can reach, as a browser would.
Send the URL
curl \
--request POST http://localhost:3000/forms/chromium/convert/url \
--form url=https://example.com \
-o example.pdf
The URL starts with http:// or https://. A file:// URL answers 400: upload the HTML to the HTML route instead.
Log in
Send the cookies a session needs. Each takes a name, a value and a domain:
curl \
--request POST http://localhost:3000/forms/chromium/convert/url \
--form url=https://example.com \
--form 'cookies=[
{ "name": "session", "value": "abc123", "domain": "example.com" }
]' \
-o example.pdf
Or send a header, such as a token. End its value with ;scope= and a regex, and Chromium sends it only to the URLs the regex matches, not to the third-party scripts the page loads:
curl \
--request POST http://localhost:3000/forms/chromium/convert/url \
--form url=https://example.com \
--form-string 'extraHttpHeaders={"Authorization":"Bearer abc123;scope=https://example\\.com/.*"}' \
-o example.pdf
The value holds a ;, so cURL sends it with --form-string: --form would cut it there. userAgent replaces Chromium's User-Agent header, for sites that answer bots differently.
Fail on errors
A page that answers a 4xx or 5xx status fails the request with 409 Conflict, rather than printing the error page. After a redirect, the status of the page it lands on counts:
curl \
--request POST http://localhost:3000/forms/chromium/convert/url \
--form url=https://example.com/missing \
-o example.pdf
failOnHttpStatusCodes changes the statuses. The resources the page loads, failed loads and the console can fail the request too:
curl \
--request POST http://localhost:3000/forms/chromium/convert/url \
--form url=https://example.com \
--form 'failOnResourceHttpStatusCodes=[
499,
599
]' \
--form failOnConsoleExceptions=true \
-o example.pdf
499 stands for every status from 400 to 499, and 599 for 500 to 599. failOnResourceLoadingFailed fails the request when a resource's load fails on the network, with a refused connection or a host that doesn't resolve for instance; a timeout or a certificate error doesn't count.
Reach private addresses
Chromium reaches any address Gotenberg's network reaches, private ones included, such as http://intranet.local/report. Operators restrict it with --chromium-deny-private-ips, --chromium-allow-list and --chromium-deny-list: see Restrict outbound requests. A blocked URL answers 403.
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/url \
--form url=https://example.com \
--form paperSize=A4 \
-o example.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/url \
--form url=https://example.com \
--form paperSize=A4 \
--form landscape=true \
-o example.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/url \
--form url=https://example.com \
--form paperWidth=210mm \
--form paperHeight=99mm \
-o example.pdf
Next to paperSize, each overrides one side.
Set the margins
curl \
--request POST http://localhost:3000/forms/chromium/convert/url \
--form url=https://example.com \
--form marginTop=2cm \
--form marginBottom=2cm \
--form marginLeft=1.5cm \
--form marginRight=1.5cm \
-o example.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/url \
--form url=https://example.com \
--form singlePage=true \
-o example.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/url \
--form files=@footer.html \
--form url=https://example.com \
-o example.pdf
header.html works the same way, at the top of each page:
curl \
--request POST http://localhost:3000/forms/chromium/convert/url \
--form files=@header.html \
--form files=@footer.html \
--form url=https://example.com \
-o example.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/url \
--form files=@header.html \
--form files=@footer.html \
--form url=https://example.com \
--form marginTop=1in \
--form marginBottom=1in \
-o example.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/url \
--form url=https://example.com \
--form printBackground=true \
-o example.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/url \
--form url=https://example.com \
--form nativeBookmarks=true \
-o example.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/url \
--form url=https://example.com \
--form nativeTaggedPdf=true \
-o example.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/url \
--form url=https://example.com \
--form 'stages=[
{ "op": "stamp-text", "text": "DRAFT", "x": 72, "y": 72 },
{ "op": "optimize" }
]' \
-o example.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.