ScreenshotNeo

BlogHTML to image & PDF

How to Convert HTML to PDF with Gotenberg

Convert local HTML and its assets to PDF with Gotenberg, choose the right route, automate requests, and fix common rendering errors.

By the ScreenshotNeo team1 October 20267 min read

Use Gotenberg’s Chromium HTML route: send a multipart POST request to /forms/chromium/convert/html, upload a required file named index.html, upload any supporting assets, and save the successful response body as a PDF.

Start Gotenberg with Docker:

docker run --rm -p "3000:3000" gotenberg/gotenberg:8

Convert a local document:

curl \
  --request POST http://localhost:3000/forms/chromium/convert/html \
  --form files=@/path/to/index.html \
  --output my.pdf

Gotenberg is a Docker-based PDF conversion API that uses Headless Chromium. The HTML route converts an index.html file and optional uploaded assets to PDF.

1. Choose the route that matches your input

Input Route Request field
Local HTML and files on your machine /forms/chromium/convert/html Multipart files, including index.html
A page already available at a URL /forms/chromium/convert/url Multipart url

Use the HTML route when your source is local. The URL route is for a reachable web page and does not accept file:// URLs. A local file sent to the URL route returns HTTP 400.

2. Prepare the local HTML and assets

Your upload must contain a file whose basename is exactly index.html. Add stylesheets, images, fonts, and other required files as additional multipart parts.

project/
├── index.html
├── styles.css
└── logo.png

Example index.html:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <link rel="stylesheet" href="styles.css">
    <title>Invoice</title>
  </head>
  <body>
    <img src="logo.png" alt="Company logo">
    <h1>Invoice #1042</h1>
    <p>Thank you for your business.</p>
  </body>
</html>

Gotenberg places uploaded files in one flat directory. Reference files by filename, such as logo.png and styles.css. Do not use absolute paths or paths such as ./assets/logo.png unless the uploaded filename itself is arranged to match that flat layout.

3. Convert HTML with cURL

Upload the document and every asset in the same request:

curl \
  --request POST http://localhost:3000/forms/chromium/convert/html \
  --form files=@/absolute/path/to/index.html \
  --form files=@/absolute/path/to/styles.css \
  --form files=@/absolute/path/to/logo.png \
  --output invoice.pdf

A successful conversion returns HTTP 200 and a PDF in the response body. The --output option writes that body directly to disk. Check the HTTP status before treating the file as valid in an automated job.

4. Convert HTML with Python

The following script sends the required HTML file and two supporting files, then writes the PDF only for a successful response:

from pathlib import Path
import requests

endpoint = "http://localhost:3000/forms/chromium/convert/html"
files = [
    ("files", ("index.html", open("index.html", "rb"), "text/html")),
    ("files", ("styles.css", open("styles.css", "rb"), "text/css")),
    ("files", ("logo.png", open("logo.png", "rb"), "image/png")),
]

try:
    response = requests.post(endpoint, files=files, timeout=90)
    response.raise_for_status()
    Path("output.pdf").write_bytes(response.content)
finally:
    for _, (_, handle, _) in files:
        handle.close()

Set the timeout high enough for the document’s rendering work and asset loads. A timeout in your client does not make a partial PDF usable; retry the conversion after checking the input and Gotenberg logs.

5. Convert HTML with Node.js

Node.js does not provide multipart encoding in every runtime, so use the form-data package:

import fs from "node:fs";
import FormData from "form-data";

const form = new FormData();
form.append("files", fs.createReadStream("index.html"));
form.append("files", fs.createReadStream("styles.css"));
form.append("files", fs.createReadStream("logo.png"));

const response = await fetch(
  "http://localhost:3000/forms/chromium/convert/html",
  { method: "POST", headers: form.getHeaders(), body: form }
);

if (!response.ok) {
  throw new Error(`Gotenberg returned ${response.status}`);
}

const pdf = Buffer.from(await response.arrayBuffer());
fs.writeFileSync("output.pdf", pdf);

6. Render a remote web page

When the source already has a reachable HTTP or HTTPS address, use the URL route:

curl \
  --request POST http://localhost:3000/forms/chromium/convert/url \
  --form url=https://example.com \
  --output page.pdf

The URL route supports JavaScript execution and dynamic content. If the page is populated asynchronously, configure a documented wait mechanism such as a fixed delay or a DOM selector before relying on the generated PDF. A file:// address is rejected; expose the document over HTTP or upload it through the HTML route instead.

7. Handle dynamic pages and failed assets

Conversion is a browser workflow: Chromium loads the document, fetches resources, runs page scripts, and produces a file. Pages that continue changing after the initial response may need request controls for a delay or an expression/selector that indicates readiness. The route also documents settings for reacting to failed asset loads.

  • Use a readiness condition when JavaScript inserts the content that must appear in the PDF.
  • Use a bounded delay for pages whose content has no reliable selector.
  • Decide how failed images, stylesheets, or fonts should affect the request instead of silently accepting an incomplete document.
  • For local conversion, upload every dependency and use flat filenames in the HTML.

8. Troubleshooting common errors

Symptom Likely cause Fix
HTTP 400 from the HTML route Invalid form fields or missing required file Send multipart data and include a file named index.html.
HTTP 400 from the URL route A file:// URL or invalid URL field Use the HTML route for local files, or provide a reachable HTTP/HTTPS URL.
HTTP 503 Conversion exceeded the configured maximum duration Inspect slow assets and scripts, add an appropriate wait condition, and retry with a bounded client timeout.
Missing images or styles Asset was not uploaded or the path does not match Gotenberg’s flat directory Upload the asset as another files part and reference only its uploaded filename.
Blank or incomplete PDF Content is rendered after the capture point or a page resource failed Wait for a selector, expression, or delay; then review failed-resource handling and browser logs.
Output is not a PDF Error response was saved without checking status Check for HTTP 200 before writing or publishing the response body.

9. Production checklist

  • Pin and record the Gotenberg version used by your deployment before depending on version-specific options.
  • Run Gotenberg in a container with port 3000 reachable from the calling service.
  • Keep uploads deterministic: include index.html and all local assets in each request.
  • Use a client timeout that covers browser startup, network loads, JavaScript, and PDF creation.
  • Check status codes and retain response headers and logs when diagnosing failures.
  • Use readiness waits for dynamic pages, but keep waits bounded so stuck pages fail predictably.
  • Load-test your own deployment and size the container host for your document complexity; the reviewed Gotenberg documentation does not provide a general throughput benchmark.

10. Performance, reliability, and cost considerations

Rendering time depends on the document, asset count, network access, JavaScript, and any configured wait. Smaller HTML and local assets remove network variability. Reuse a running Gotenberg service rather than starting a new container for every request, and monitor conversion duration and HTTP failures in your own environment.

Gotenberg itself is documented as a Docker-based API. Your infrastructure cost therefore comes from the container host and its resources; the supplied documentation does not establish a particular hosting provider, price, uptime target, or benchmark. Treat 503 responses as conversion timeouts, correct the underlying page or timeout configuration, and retry only when the operation is safe to repeat.

11. Or skip the browser setup

If you need a screenshot or PDF of a publicly reachable page rather than a self-hosted HTML rendering service, ScreenshotNeo provides a single GET request. See the ScreenshotNeo API docs for the available options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server lets AI agents take screenshots, inspect pages, and capture PDFs. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

12. FAQ

Does Gotenberg require the file to be named index.html?

Yes. The documented HTML route requires an uploaded file named index.html.

Can I upload a directory tree?

Uploads are stored in a flat directory. Send each required file and reference it by filename.

Which route should I use for Markdown?

The reviewed material covers the Chromium HTML and URL routes. Use the route documented for the input format in the Gotenberg version you deploy.

Why did a URL conversion finish before my data appeared?

The page likely renders content asynchronously. Configure a documented delay or readiness selector/expression and keep the wait bounded.

What does HTTP 503 mean?

The conversion did not complete within the configured maximum duration. Check slow resources, scripts, readiness waits, and the document itself.