Home / Docs / POST /v1/convert

POST /v1/convert

Updated Apr 2026 6 min read v1 stable

Submit HTML (or a URL) and get a PDF back. Conversions are enqueued on the server's worker pool and returned asynchronously: the request gives you a job_id; poll the job endpoint to track progress and download the rendered file.

Overview

The request body takes one of three input shapes — html, url, or simple_html — along with an optional options object that maps 1:1 onto Chromium's print-to-PDF parameters. The response returns a job identifier; the actual render happens in a pool of headless Chromium workers and the resulting PDF is stored in object storage until you fetch it.

NOTE

The rendering engine is Chromium. The previous wkhtmltopdf engine was removed in April 2026 — all conversions now go through the modern engine, which is what gives you reliable CSS Grid, flexbox, web fonts, and print media queries.

Authentication

Authenticate with an API key in the Authorization header. Generate keys in the dashboard at login.21pdf.com/api-keys. Every key is scoped to a single tenant and carries the plan's quota and concurrency limit.

Authorization header
Authorization: Bearer qpdf_live_…

Quick start

Convert a URL to a Letter-size landscape PDF:

POST https://login.21pdf.com/v1/convert
curl -X POST https://login.21pdf.com/v1/convert \
  -H "Authorization: Bearer $QPDF_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/invoice/8412",
    "options": {
      "page_size": "Letter",
      "orientation": "landscape",
      "wait_for_network_idle": true
    }
  }'

The response tells you where to pick up the result:

{
  "job_id": "5f1c3e2a-9b4d-4c8a-9e2f-1a2b3c4d5e6f",
  "status": "pending",
  "message": "enqueued"
}

Sources

Exactly one of these fields must be present in the request body. Sending more than one is an error; sending none is an error.

Field
Type
Description
htmlmax 10 MB
string
A full HTML document. Remote assets (CSS, fonts, images) are fetched during render subject to the SSRF policy below.
urlmust be http(s)
string
A URL to fetch and render. The Chromium worker loads the page and prints it. Private-IP targets are rejected (see SSRF).
simple_htmlboolean, with html
boolean
Hint that the HTML body doesn't need network access. Skips the network-idle wait for faster turnaround.

PDFOptions

All fields are optional; defaults match Chromium's built-in print defaults. Values are strings with CSS length units (mm, in, cm, px) unless otherwise noted.

Field
Type
Description
page_size
string
One of A4, Letter, Legal, A3, A5, Tabloid. Default A4.
orientation
string
portrait (default) or landscape.
margin_top
string
e.g. "20mm". Default "10mm".
margin_bottom
string
Same format as margin_top.
margin_left
string
Same format. Default "10mm".
margin_right
string
Same format.
wait_for_network_idle
boolean
If true, wait for the page to reach network-idle before printing. Useful for SPAs and pages with deferred font/image loading.
HONESTY

Custom header/footer HTML, pagination tokens (pageNumber, totalPages), and webhook delivery are not in the current API. They're on the roadmap. If you need them today, inline them into your source HTML and use CSS @page rules.

Response & job lifecycle

A successful submission returns HTTP 202 with:

Field
Type
Description
job_id
uuid
Use this to poll status and download the result.
status
string
Starts as pending; transitions to running, then completed or failed.
message
string
Human-readable state note; for failed jobs this holds the Chromium error string.

Polling a job

Poll GET /v1/jobs/{job_id} until status is completed. A reasonable backoff is 500 ms, doubling up to 5 s. Jobs typically finish in 1–3 s; pages that wait for network idle can take longer.

Downloading the PDF

Once the job is completed, fetch GET /v1/jobs/{job_id}/download. The response is the raw PDF bytes with Content-Type: application/pdf. Rendered files are retained for the period set by PDF_RETENTION_DAYS on the deployment (default 7 days), after which the orphan-sweeper deletes them from object storage.

Per-plan quotas

Every API key is attached to a subscription. When a plan's quota_per_cycle is reached, further /v1/convert requests return HTTP 402 until the cycle resets or you upgrade. The internal plan has quota_per_cycle = 0, which means "unlimited" (used for first-party tenants).

concurrency_limit caps how many jobs can be in the running state at once for a given tenant; excess submissions stay in pending until a slot frees up.

SSRF / private-IP block

When you pass a url, the renderer resolves it and refuses to load any target in the RFC 1918 private ranges, link-local ranges, or the loopback subnet. This check runs twice: at the HTTP boundary and again inside Chromium's request interceptor, so a redirect that lands on a private IP still fails. Both layers must fail-closed for an SSRF to land.

TIP

Need to render a document behind a private network? POST the HTML directly (as html) — the renderer won't fetch anything it doesn't already have, so SSRF doesn't apply.