Workflow automation

# How to convert HTML to PDF at scale

Render invoices, reports, and receipts to PDF from a URL or uploaded HTML, and make the output repeatable.

Published August 13, 2026

## Key takeaways

* Set the pdf format on /html/convert; the other formats produce screenshots instead.
* Control when the snapshot is taken with wait\_until and, only when necessary, delay.
* Send authentication through headers rather than embedding credentials in the URL.

Most PDF requirements start as a page that already renders correctly in a browser. Rendering that page server-side is usually cheaper than maintaining a second layout in a PDF library, provided the timing and the inputs are controlled.

## In this guide

1. [Render the page you already have](#convert-html-to-pdf-section-1)
2. [Control when the snapshot is taken](#convert-html-to-pdf-section-2)
3. [Reach protected pages without leaking credentials](#convert-html-to-pdf-section-3)
4. [Make a reissued document identical to the original](#convert-html-to-pdf-section-4)
5. [Keep the cost of rendering predictable](#convert-html-to-pdf-section-5)
6. [Verify the document before a customer sees it](#convert-html-to-pdf-section-6)

## What matters most

* Render from a stable, versioned template URL so a design change cannot alter an issued document.
* Merge multi-part documents with /document/merge instead of concatenating PDFs yourself.

## Render the page you already have

Most PDF requirements begin as a page that already renders correctly in a browser: an invoice, a statement, a report. Maintaining a second layout in a PDF library duplicates that work and guarantees the two will drift. `/html/convert` renders the page with a headless browser and returns the result, and setting `format: "pdf"` is what distinguishes a document from a screenshot. The same Robot produces `jpeg`, `jpg`, and `png`, which are images of the page rather than paginated documents.

The Robot accepts either a `url` to render or an uploaded HTML file. Rendering a URL is usually the better choice for documents that already exist as pages, because it keeps one source of truth. Uploading HTML suits documents assembled on the fly, where no stable URL exists.

Render a versioned invoice URL to PDF and store the result

```
{
  "steps": {
    "rendered": {
      "robot": "/html/convert",
      "url": "https://example.com/inv/1043?v=3",
      "format": "pdf",
      "wait_until": "networkidle"
    },
    "exported": {
      "use": "rendered",
      "robot": "/s3/store",
      "credentials": "my_s3_credentials",
      "path": "inv/1043.pdf"
    }
  }
}
```

### format: "pdf"

Produces the document. The other formats capture an image of the page instead.

### url or upload

Render an existing page by URL, or upload generated HTML when no stable URL exists.

### omit\_background

Applies to image output only. Transparency cannot be carried into a PDF.

## Control when the snapshot is taken

The most common failure is a document that renders correctly by hand and arrives half-empty from the Robot, because the snapshot was taken before fonts loaded or a chart finished drawing. `wait_until` maps to the browser load state and is the precise way to express that dependency. Choosing the right load state fixes most timing problems without adding fixed latency.

`delay` adds a fixed pause afterwards. It is occasionally necessary for animations or third-party widgets that report ready before they are, but it costs that pause on every single render, including the ones that did not need it. Reach for a more specific `wait_until` first, and treat `delay` as the fallback rather than the default.

### wait\_until

Expresses the actual dependency on the browser load state. Prefer it.

### delay

A fixed pause charged on every render. Use only when a load state cannot express the wait.

### Print stylesheet

Verify the page in a browser print preview before rendering it server-side.

## Reach protected pages without leaking credentials

Because rendering runs a real browser, the page has to be reachable from Transloadit. Documents usually live behind authentication, which leaves two workable options. The `headers` parameter passes authentication with the request, which suits token-based access. Alternatively, issue a short-lived signed URL that grants access to exactly one document for a short window.

Putting credentials in the query string of the rendered URL is the option to avoid. Those URLs end up in logs and in the stored record of what was rendered, and unlike a header they are trivially reusable if the record is ever exposed.

Authenticate with a header instead of a query string

```
{
  "steps": {
    "rendered": {
      "robot": "/html/convert",
      "url": "https://example.com/reports/q3",
      "format": "pdf",
      "wait_until": "networkidle",
      "headers": [
        "Authorization: Bearer ${fields.token}"
      ]
    }
  }
}
```

### headers

Carries tokens with the request rather than in the URL, so they stay out of logs.

### Signed one-time URLs

Grant access to a single document for a short window when header auth is not available.

### Never in the query string

Credentials placed there are recorded wherever the rendered URL is stored.

## Make a reissued document identical to the original

An invoice is a legal record, and the version a customer receives in March should still render identically in November. Two habits achieve that. Render from a versioned template URL so that a later design change cannot alter a document that has already been issued, and store the resulting file rather than regenerating it on demand.

Documents assembled from several parts are worth handling explicitly. `/document/merge` combines rendered pages into one file inside the same Assembly, which keeps ordering deterministic and avoids a second service that has to be given access to the parts.

Merge multi-part documents inside one Assembly

```
{
  "steps": {
    "cover": {
      "robot": "/html/convert",
      "url": "https://example.com/stmt/cover?v=3",
      "format": "pdf",
      "wait_until": "networkidle"
    },
    "detail": {
      "robot": "/html/convert",
      "url": "https://example.com/stmt/detail?v=3",
      "format": "pdf",
      "wait_until": "networkidle"
    },
    "statement": {
      "use": ["cover", "detail"],
      "robot": "/document/merge"
    }
  }
}
```

### Version the template

A design change should produce new documents, not retroactively change issued ones.

### Store, do not regenerate

Keep the produced file so a reissue is a copy rather than a fresh render.

### /document/merge

Combines multi-part documents in one Assembly with deterministic ordering.

## Keep the cost of rendering predictable

A page render is more expensive than a format conversion, because it starts a browser, fetches subresources, and waits for the page to settle. That cost is fine for a document a customer asked for, and wasteful when the same statement is rendered afresh every time somebody opens a list view. The usual fix is to render once at the moment the document becomes final, then serve the stored file.

Bulk generation deserves separate treatment. A month-end run that produces thousands of statements should not compete with the render a customer is waiting on, and a fixed `delay` applied across such a batch multiplies into real time and money. Measuring cost per issued document, rather than per Assembly, tends to expose these patterns quickly.

### Render at finalization

Produce the file when the document becomes final, not on every view.

### Separate bulk runs

Keep month-end batches away from renders a person is waiting for.

### Audit fixed delays

A one-second pause is invisible once and expensive across ten thousand documents.

## Verify the document before a customer sees it

A render can succeed and still be wrong. The Robot returns a valid PDF whether or not the chart drew, so a check that only asks whether a file was produced will not catch a blank page. Cheap assertions catch most of it: a plausible byte size, an expected page count, and the presence of a known string such as the document number.

Rendering to `png` alongside the PDF during development gives a fast visual check that is easy to eyeball in review, and comparing a fresh render against a stored reference image catches layout regressions that a byte-size check cannot. Neither belongs in the production path, but both are worth having in the pipeline that ships template changes.

### Assert content, not existence

Check page count and a known identifier rather than only that a file exists.

### Image renders for review

A `png` of the same page makes template changes reviewable at a glance.

### Compare against a reference

Visual comparison catches layout regressions that size checks will miss.

## Technical details worth knowing

* The format parameter accepts jpeg, jpg, pdf, and png. Only pdf produces a document; the rest capture an image of the page.
* The omit\_background parameter applies to image output and has no effect when format is pdf, so transparency cannot be carried into the document.
* The wait\_until parameter maps to the underlying browser load state, which is the reliable way to wait for fonts, charts, and late-loading data before the snapshot is taken.
* The delay parameter adds a fixed pause after the load state is reached. It is a blunt instrument that raises cost and latency on every render, so prefer a more specific wait\_until where possible.
* A rendered invoice is a legal record. Rendering from an immutable template URL, and storing the resulting file rather than regenerating it on demand, keeps a reissued copy identical to the original.
* Because rendering runs a real browser, the page must be reachable from Transloadit. Pages behind a session cookie need either a signed one-time URL or the credentials passed through the headers parameter.

## A practical approach

1. 1\
   Build the document as a normal page with a print stylesheet and verify it in a browser first.
2. 2\
   Render it with /html/convert using the pdf format and an explicit wait\_until.
3. 3\
   Store the result in your own bucket with the identifiers that produced it.
4. 4\
   Merge supporting pages into one file with /document/merge when the document has several parts.

A four-stage media workflow

## When Transloadit is useful

Use /html/convert with the pdf format when the document already exists as a web page or can be rendered as one. Point it at a url, or upload HTML and let the Robot render the uploaded file. Combine it with /document/merge when several pages belong in one file.

## Architecture boundary

/html/convert renders a page with a headless browser, so it produces a paginated visual copy rather than a tagged, accessible PDF. Documents that need selectable structure, form fields, or long-term archival formats such as PDF/A should be produced by a dedicated document generator.

## Frequently asked questions

### Why is my PDF missing charts or fonts?

The snapshot was almost certainly taken before those finished loading. Set `wait_until` to a load state that covers the dependency. Add `delay` only if a load state cannot express it, remembering that the pause is charged on every render.

### Can I produce a PDF with a transparent background?

No. `omit_background` affects image output, and has no effect when `format` is `pdf`. If transparency is required, render to `png` instead and place that image into a document.

### How do I render a page that requires a login?

Pass authentication through the `headers` parameter, or issue a short-lived signed URL scoped to the single document. Avoid putting credentials in the query string, because the rendered URL is recorded wherever the render is logged.

### Is the output an accessible, tagged PDF?

No. A headless browser produces a paginated visual copy, not a tagged document with a reading order, form fields, or PDF/A conformance. Requirements of that kind need a dedicated document generator rather than a page render.

### How do I combine several rendered pages into one file?

Render each part and pass the results to /document/merge in the same Assembly. Keeping the merge inside the Assembly makes the ordering deterministic and avoids granting another service access to the individual parts.

## Build the workflow

Move from the concept to a tested Assembly with Robot documentation and working demos.

### Relevant Robots

* [/html/convert](/docs/robots/html-convert.md)
* [/document/merge](/docs/robots/document-merge.md)
* [Read the API documentation](/docs.md)
* [Explore working demos](/demos.md)
* [Create a free workspace](/c/signup/)

Workflow automation

## Continue with related guides

* [How to extract text from documents and images at scale](/guides/extract-text-from-documents-and-images.md)\
  Recognise text across PDFs, scans, and photographs, and keep the result attached to the file it came from.
* [Media automation: from upload to reliable output](/guides/media-automation.md)\
  Automate repeatable media intake, transformation, validation, and export while preserving observability and control.
* [A complete guide to digital-asset workflows](/guides/digital-asset-workflows.md)\
  Design a digital-asset workflow from intake and processing through review, publication, retention, and deletion.
* [AI content moderation in an upload workflow](/guides/ai-content-moderation-workflows.md)\
  Place AI moderation inside a controlled upload workflow with confidence thresholds and human review.
* [Automated content moderation: architecture and failure handling](/guides/automated-content-moderation.md)\
  Build automated moderation as a layered system of file checks, classifiers, policy decisions, and review queues.
* [Automated image analysis with observable workflows](/guides/automated-image-analysis.md)\
  Turn image analysis into a repeatable, asynchronous workflow rather than a blocking application request.
