Screenshot HTML
Captures a screenshot of an index.html file (and optional assets) using Headless Chromium. Reference additional CSS, images, and fonts in your HTML using relative paths.
See the Chromium module configuration for startup and behavior flags.
Basics
curl \
--request POST http://localhost:3000/forms/chromium/screenshot/html \
--form files=@/path/to/index.html \
-o my.jpeg
- 200
- 400
- 503
Content-Disposition: attachment; filename={output-filename.ext}
Content-Type: {content-type}
Content-Length: {content-length}
Gotenberg-Trace: {trace}
Body: {output-file}
Content-Type: text/plain; charset=UTF-8
Gotenberg-Trace: {trace}
Body: {error}
Content-Type: text/plain; charset=UTF-8
Gotenberg-Trace: {trace}
Body: Service Unavailable
Assets
Send images, fonts, and stylesheets alongside your HTML.
curl \
--request POST http://localhost:3000/forms/chromium/screenshot/html \
--form files=@/path/to/index.html
--form files=@/path/to/img.png \
-o my.jpeg
All uploaded files are stored in a single flat directory. Reference assets by filename only: no absolute paths (/img.png) or subdirectories (./assets/img.png).
✅ Correct Reference
Since index.html and logo.png sit side-by-side in the container:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>My PDF</title>
</head>
<body>
<h1>Hello world!</h1>
<img src="logo.png" />
</body>
</html>
❌ Incorrect Reference
Gotenberg does not recreate your local folder structure (e.g., images/) inside the container.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>My PDF</title>
</head>
<body>
<h1>Hello world!</h1>
<img src="/images/logo.png" />
</body>
</html>
For remote URLs (CDNs, Google Fonts), make sure the container can resolve the domain. You cannot use localhost from inside the container. Use host.docker.internal or your host's network IP instead.
For small assets, Base64 data URIs eliminate network dependencies entirely. Avoid the HTML <base> element: it breaks the link between your HTML and uploaded assets.
Rendering Behavior
800600falsepng100falsefalsecurl \
--request POST http://localhost:3000/forms/chromium/screenshot/html \
--form files=@/path/to/index.html \
--form width=1280 \
--form height=720 \
--form format=jpeg \
--form quality=85 \
--form optimizeForSpeed=true \
-o my.jpeg
Viewport & Layout
The viewport defines the image. Unlike PDFs which paginate content, screenshots capture the browser's viewport exactly as rendered.
- Resolution: Ensure you set the
widthandheightform fields to match your desired target device (e.g.,1920x1080for desktop,375x812for mobile). - If your screenshot's content is repeated and clipped, consider setting the
skipNetworkIdleEventform field to false (see issue #1065).
If you are simulating a mobile device, remember to also set the userAgent to match a mobile browser, or the site might serve the desktop version.
Emulated Media Features
You can simulate specific browser conditions by overriding CSS media features. This is particularly useful for forcing "Dark Mode" or testing layouts with reduced motion.
The emulatedMediaFeatures field expects a JSON array of objects, where each object contains a name and a value.
Common Media Features:
| Feature Name | Common Values | Description |
|---|---|---|
prefers-color-scheme | light, dark | Emulates the user's OS color theme preference. |
prefers-reduced-motion | no-preference, reduce | Emulates the setting to minimize non-essential motion. |
color-gamut | srgb, p3, rec2020 | Emulates the approximate range of colors supported by the output device. |
forced-colors | none, active | Emulates "High Contrast" modes where the browser restricts colors. |
Nonecurl \
--request POST http://localhost:3000/forms/chromium/screenshot/html \
--form files=@/path/to/index.html \
--form emulatedMediaFeatures='[{"name": "prefers-color-scheme", "value": "dark"}, {"name": "prefers-reduced-motion", "value": "reduce"}]' \
-o my.jpeg
Image Format
Choosing the right output format impacts quality and file size:
- PNG: Best for screenshots containing text, UI elements, or flat colors. It is lossless, ensuring text remains sharp.
- JPEG: Best for screenshots containing photographs or complex gradients. Use the
qualityform field to balance file size against visual fidelity (default is100). - WebP: Offers a modern balance of compression and quality, usually superior to JPEG.
JavaScript & Dynamic Content
Chromium captures what is currently visible. If the page relies on JavaScript to render data, charts, or external content, the conversion might trigger before the rendering is complete, resulting in blank or incomplete sections.
If the content is generated dynamically:
- Use the
waitDelayform field to add a fixed pause before conversion. - For more precision, use the
waitForExpressionform field to trigger the conversion only when a specific JavaScript condition (e.g.,window.status === 'ready') is met.
Wait Delay
Use this as a fallback when you cannot modify the target page's code. It forces Gotenberg to wait for a fixed duration before rendering, giving JavaScript time to finish execution.
This method is "brute force". If the page loads faster, time is wasted. If it loads slower, the image will be incomplete. Use explicit waits (Expression or Selector) whenever possible.
curl \
--request POST http://localhost:3000/forms/chromium/screenshot/html \
--form files=@/path/to/index.html \
--form waitDelay=5s \
-o my.jpeg
Wait For Expression
This is the most robust method for synchronization. It pauses the conversion process until a specific JavaScript expression evaluates to true within the page context. This ensures the image is generated exactly when your data is ready.
Example: Client-side logic
// Inside your HTML page.
window.status = "loading";
fetchData().then(() => {
renderCharts();
// Signal to Gotenberg that the page is ready.
window.status = "ready";
});
curl \
--request POST http://localhost:3000/forms/chromium/screenshot/html \
--form files=@/path/to/index.html \
-- form 'waitForExpression=window.status === '\''ready'\''' \
-o my.jpeg
Wait For Selector
Ideally suited for Single Page Applications (SPAs) or frameworks like React/Vue. This method delays the conversion until a specific HTML element - identified by a CSS selector - appears in the DOM.
Example: Dynamic Element Injection
// Inside your HTML page.
await heavyCalculation();
const completionMarker = document.createElement("div");
completionMarker.id = "app-ready"; // The selector we wait for.
document.body.appendChild(completionMarker);
curl \
--request POST http://localhost:3000/forms/chromium/screenshot/html \
--form files=@/path/to/index.html \
--form 'waitForSelector=#app-ready' \
-o my.jpeg
HTTP & Networking
Cookies
A JSON array of cookie objects for authentication or session state.
| Key | Description | Default |
|---|---|---|
name | The name of the cookie. | Required |
value | The value of the cookie. | Required |
domain | The domain the cookie applies to (e.g., example.com). | Required |
path | The URL path the cookie applies to. | None |
secure | If true, the cookie is only sent over HTTPS. | None |
httpOnly | If true, the cookie is inaccessible to JavaScript (document.cookie). | None |
sameSite | Controls cross-site behavior. Values: "Strict", "Lax", "None". | None |
Cookies expire when the request reaches its time limit. For strict isolation between conversions, configure the API to clear the cookie jar after every request. See API Configuration.
Nonecurl \
--request POST http://localhost:3000/forms/chromium/screenshot/html \
--form files=@/path/to/index.html \
--form 'cookies=[{"name":"yummy_cookie","value":"choco","domain":"theyummycookie.com"}]' \
-o my.jpeg
HTTP Headers
A JSON object of headers sent with every browser request (including images, stylesheets, scripts).
To restrict a header to specific URLs, append ;scope= with a regex. The scope token is stripped before sending.
Example: "X-Internal-Token": "secret-123;scope=.*\\.internal\\.api"
https://data.internal.api/v1→ header senthttps://google.com/fonts→ header not sent
NoneNonecurl \
--request POST http://localhost:3000/forms/chromium/screenshot/html \
--form files=@/path/to/index.html \
--form 'userAgent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)"' \
--form-string 'extraHttpHeaders={"X-Header":"value","X-Scoped-Header":"value;scope=https?:\/\/([a-zA-Z0-9-]+\.)*domain\.com\/.*"}' \
-o my.jpeg
Invalid HTTP Status Codes
Return a 409 Conflict if the main page or its resources return specific HTTP status codes (JSON array of integers).
| Field | Description |
|---|---|
failOnHttpStatusCodes | Fails if the main page URL returns a matching code. |
failOnResourceHttpStatusCodes | Fails if any asset (image, CSS, script) returns a matching code. |
Status Code Ranges
Ranges use the X99 notation:
499matches every code from 400 to 499.599matches every code from 500 to 599.
Domain Exclusions
Exclude third-party assets (analytics, tracking scripts) from status code checks with ignoreResourceHttpStatusDomains. Matches if the hostname equals or is a subdomain of the entry. Values are normalized (trimmed, lowercased, port/scheme stripped):
example.com*.example.comor.example.comexample.com:443(port is ignored)https://example.com/path(scheme/path are ignored)
[499,599]NoneNonecurl \
--request POST http://localhost:3000/forms/chromium/screenshot/html \
--form files=@/path/to/index.html \
--form 'failOnHttpStatusCodes=[499,599]' \
--form 'failOnResourceHttpStatusCodes=[499,599]' \
--form 'ignoreResourceHttpStatusDomains=["sentry-cdn.com","analytics.example.com"]' \
-o my.jpeg
- 409
Content-Type: text/plain; charset=UTF-8
Gotenberg-Trace: {trace}
Body: Invalid HTTP status code from the main page: 400: Bad Request
Network Errors
If the browser hits a critical network error on the main page, the API returns 400 Bad Request:
net::ERR_CONNECTION_CLOSEDnet::ERR_CONNECTION_RESETnet::ERR_CONNECTION_REFUSEDnet::ERR_CONNECTION_ABORTEDnet::ERR_CONNECTION_FAILEDnet::ERR_NAME_NOT_RESOLVEDnet::ERR_INTERNET_DISCONNECTEDnet::ERR_ADDRESS_UNREACHABLEnet::ERR_BLOCKED_BY_CLIENTnet::ERR_BLOCKED_BY_RESPONSEnet::ERR_FILE_NOT_FOUNDnet::ERR_HTTP2_PROTOCOL_ERROR
By default, Chromium does not wait for network activity to settle before converting. Two form fields control this behavior:
skipNetworkIdleEventset tofalse: waits until zero open connections persist for 500ms. Strictest option, but pages with long-polling or analytics connections may never reach this state.skipNetworkAlmostIdleEventset tofalse: waits until at most 2 open connections persist for 500ms. A practical middle ground for pages that keep a background connection alive (WebSockets, analytics pings, heartbeat polls).
Both default to true (no wait). If both are set to false, both events must fire before conversion proceeds.
A broken image or stylesheet won't fail the conversion on its own. The PDF is generated with missing assets. Set failOnResourceLoadingFailed to true to fail on any resource network error.
truetruefalsecurl \
--request POST http://localhost:3000/forms/chromium/screenshot/html \
--form files=@/path/to/index.html \
--form skipNetworkIdleEvent=false \
--form skipNetworkAlmostIdleEvent=false \
--form failOnResourceLoadingFailed=true \
-o my.jpeg
Console
falsecurl \
--request POST http://localhost:3000/forms/chromium/screenshot/html \
--form files=@/path/to/index.html \
--form failOnConsoleExceptions=true \
-o my.jpeg
- 409
Content-Type: text/plain; charset=UTF-8
Gotenberg-Trace: {trace}
Body:
Chromium console exceptions:
exception "Uncaught" (17:10): Error: Exception 1
at file:///tmp/db09d2c8-31e3-4058-9923-c2705350f2b3/index.html:18:11;
exception "Uncaught" (20:10): Error: Exception 2
at file:///tmp/db09d2c8-31e3-4058-9923-c2705350f2b3/index.html:21:11:

