ServiceLabs

HTML to PDF

Send a link to an HTML document. Get back a PDF printed by headless Chromium, at the size you ask for or the size the document asks for.

SyncPOST /v1/utilities/html-pdfWaits up to 90 s for the finished job.

AsyncPOST /v1/utilities/html-pdf/jobsAnswers 202 with the job id at once.

What it’s for

  • Invoices and receiptsPrint the template you already render on screen. Its @page rules, web fonts and symbols like ₹ come through.
  • Statements and reportsLong documents with many images go through the async route and come back as one file.
  • Labels, tickets, odd sizesSet width and height in px, in, cm or mm, and turn the page with orientation, without touching the template.
  • Templates kept in S3Point at an s3:// URL. If the public fetch fails, the object is read directly from the utilities’ bucket.

How it runs

  1. FetchDownloads the HTML from your URL. A fragment without an <html> tag is wrapped in a UTF-8 document with Noto Sans.
  2. SettleWaits for the network to go idle, then for document.fonts.ready, then 500 ms more.
  3. PrintChromium prints with your pdfOptions. Without them, the document’s own @page size wins.
  4. StoreOne PDF, linked on the job as result.files[0].

pdfOptions is checked before Chromium starts. A bad option fails in milliseconds with 400, and no job is created.

Input

Send these fields as input in the request body. Anything not listed is refused with 400.

Field Type Required or optional Accepts
url string Required http://, https:// or s3:// URL, 1–4,096 characters. The HTML document to render.
pdfOptions object Optional Page geometry. Leave it out to keep the document’s own @page size. Unknown keys are refused.
pdfOptions.format string Optional Letter, Legal, Tabloid, Ledger or A0A6, any case. Default A4. Not together with width and height.
pdfOptions.width number | string with height A number is pixels. A string of 1–32 characters may carry px, in, cm or mm: "21cm", "8.5in".
pdfOptions.height number | string with width As width. Describe the page upright and use orientation to turn it.
pdfOptions.orientation string Optional portrait or landscape, any case. Swaps the page’s dimensions.
pdfOptions.landscape boolean Optional The same flag as orientation. If you send both, they must agree.
pdfOptions.scale number Optional 0.1 to 2. Default 1.
pdfOptions.preferCSSPageSize boolean Optional Let the document’s @page rule win outright. Default false.
pdfOptions.printBackground boolean Optional Print background colours and images. Default true.
pdfOptions.margin object Optional { top, right, bottom, left }, each a number (px) or a length string. Default 1cm on every side.

How the page size is decided

You send The PDF gets
No pdfOptions, or {} The document’s own @page size, unchanged.
Only format Dimensions from format. A document @page rule can still set the orientation.
width/height, orientation or landscape Your geometry. The document’s @page size is overridden; its margins survive.
preferCSSPageSize: true The document’s @page rule, outright.

The request body

Field Required or optional What it is
input Required The fields above.
metadata Optional Your own key–value pairs, returned on the job. At most 50 keys, 4 KB serialised.
callbackUrl Optional Async route only. The finished job is POSTed here, signed. Up to 2,048 characters.

What comes back

Field Value
files[].url A link to the file.
result.files Exactly one file.
files[].name A generated yyyymmddhhmmss_<10 hex>.pdf name.
files[].contentType application/pdf

Example

curl https://api.servicelabs.dev/v1/utilities/html-pdf \
  -H "Authorization: Bearer $DU_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": {
      "url": "https://example.com/invoice.html",
      "pdfOptions": {
        "format": "A4",
        "printBackground": true,
        "margin": { "top": "12mm", "bottom": "12mm" }
      }
    }
  }'
Response · 200 OK
{
  "id": "job_01K7Z3Q4N5P6R7S8T9V0W1X2Y3",
  "object": "job",
  "utility": "html-pdf",
  "mode": "sync",
  "status": "succeeded",
  "createdAt": "2026-09-10T17:30:06.650Z",
  "finishedAt": "2026-09-10T17:30:09.009Z",
  "timings": { "queueMs": 0, "upstreamMs": 2140, "storageMs": 196, "totalMs": 2359 },
  "result": {
    "files": [
      {
        "url": "https://…/20260910173009_22d0cf84ca.pdf",
        "name": "20260910173009_22d0cf84ca.pdf",
        "size": 115741,
        "contentType": "application/pdf"
      }
    ]
  },
  "error": null,
  "links": {
    "self": "/v1/jobs/job_01K7Z3Q4N5P6R7S8T9V0W1X2Y3"
  }
}

An example job. File links point at the stored output; download anything you need to keep.

Bad input · 400
{
  "error": {
    "code": "invalid_request",
    "message": "input failed validation for html-pdf",
    "details": [
      {
        "path": "input.url",
        "message": "must be an http://, https:// or s3:// URL"
      }
    ]
  }
}

Input is checked before anything runs. A 400 creates no job and costs nothing.

Limits

Limit Value
Runs up to 900 s (15 min)
Sources http://, https:// or s3://
Output One application/pdf file
Page load Network idle, then fonts, then 500 ms
Defaults Backgrounds printed, 1cm margins
Sync wait 90 s, then 202 with the job id
Long-poll Up to 60 s per GET /v1/jobs/{id}?wait=
Rate limit Per key; 600 requests a minute unless set otherwise
Output files Kept for a limited period; download what you need

When it fails

A 400 means nothing ran. Everything else is a failed job with status: "failed", an error code and the utility’s own message.

Situation Status Code Job
url isn’t http(s):// or s3://; an unknown or contradictory pdfOptions key; a bad format 400 invalid_request No job
The HTML can’t be downloaded (404, DNS, a private URL) 422 upstream_rejected Failed job
orientation contradicts landscape 422 upstream_rejected Failed job
The render crashed, e.g. out of memory on a huge page 502 upstream_error Failed job
The render ran past 900 s 504 upstream_timeout Failed job

Any call can also get 401 unauthorized, 403 forbidden (the key isn’t allowed this utility) or 429 rate_limited. The full list is in Conventions & errors.

Reference

Also in service

Try HTML to PDF on your own file.

The console’s playground runs html-pdf against any URL you give it and shows the job it returns.