POST /v1/convert
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.
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: Bearer qpdf_live_…
Quick start
Convert a URL to a Letter-size landscape PDF:
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.
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.
A4, Letter, Legal, A3, A5, Tabloid. Default A4.portrait (default) or landscape."20mm". Default "10mm".margin_top."10mm".true, wait for the page to reach network-idle before printing. Useful for SPAs and pages with deferred font/image loading.
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:
pending; transitions to running, then completed or failed.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.
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.