/ Features
Every knob we ship.
Nothing we don't.
A full list of what 21pdf does, organised by concern. Every feature is on every plan — plans gate throughput, not capability.
Rendering
Chromium — the same engine your users will run the output through.
Chromium-only
Headless Chromium. Replaced wkhtmltopdf in April 2026 — same paginator, modern flex/grid support.
Paper sizes
A3, A4, A5, Letter, Legal, Tabloid via PDFOptions.page_size.
Orientation
Portrait or landscape via PDFOptions.orientation.
Margins
Per-side in mm: margin_top, margin_bottom, margin_left, margin_right.
Network-idle wait
Hold the render until XHRs settle — wait_for_network_idle: true.
CSS @page respected
Print media queries applied; your @page size/margins override PDFOptions.
Inputs
Three shapes. Pick the one that fits the pipeline upstream of you.
Raw HTML
POST a full HTML payload in the request body under html.
URL fetch
Send url; we fetch the page server-side and render it. SSRF-checked before fetch.
simple_html shortcut
Fragment-level input. We wrap it in a minimal document before rendering.
Inline CSS & fonts
System fonts + any @font-face loaded via your HTML. No font upload API.
No markdown
Not in v1. Pre-render to HTML client-side — many good libraries exist.
Sanitisation
None. The renderer trusts your HTML. Don’t pass unsanitised user input.
Job model
Async by default — the worker pool keeps the HTTP surface fast.
Submit → pool → download
POST returns job_id immediately. Polling endpoint reports queued → processing → succeeded|failed.
Polling endpoint
GET /v1/jobs/:id returns status + PDF pointer when ready.
Signed download
GET /v1/jobs/:id/download streams the PDF from MinIO — single-use within the retention window.
Concurrency gate
Per-plan in-flight limit — 1 Free, 2 Starter, 3 Pro, 5 Business. 429 with Retry-After if exceeded.
Timeouts
Per-job render timeout. Failed jobs do NOT count toward your quota.
Orphan sweeper
Retention worker deletes PDFs older than PDF_RETENTION_DAYS — no stale storage cost.
Auth & keys
Bearer tokens. JWT for humans, opaque API keys for machines.
Bearer JWT
Short-lived tokens from /v1/auth/login. 3-tier RBAC: user, admin, superadmin.
Per-user API keys
qpdf_live_* and qpdf_test_* prefixes. Rotate + revoke anytime from the dashboard.
Integration keys
Named keys for internal projects (bill21-prod, etc.) — tenant-scoped, unlimited.
Key in header
Authorization: Bearer qpdf_live_... — same header as the JWT path.
Audit log
Every key use lands in audit_log with request-id, user-id, action, result.
No SDK yet
Raw REST on purpose. curl + your HTTP client of choice. SDKs are roadmap.
Security
Boring defences that actually matter for a URL-fetching service.
HTTP-boundary block
Reject 127.0.0.0/8, 169.254.0.0/16, 10/8, 172.16/12, 192.168/16, fe80::/10, and link-local before a fetch.
Chromium interceptor
Second-layer check inside the browser — every sub-request the page makes is re-validated.
SSE-S3 at rest
MinIO server-side encryption for PDFs in transit to/from storage.
No inbound TLS inside docker
Single-host bridge; internal TLS declined as redundant. Public TLS at edge.
Tamper-evident log
audit_log captures auth, billing, admin actions. Append-only from application.
Lockout on brute force
Email/password login lockout after repeated failures.
Billing & ops
Razorpay subscriptions wired end-to-end.
Razorpay subscriptions
Hosted checkout. We never touch PAN — PCI scope stays at SAQ A.
Quota headers
X-Quota-Used + X-Quota-Limit on /v1/convert so clients self-rate-limit.
7-day grace + dunning
Nag emails on day 1/3/5 after failed payment; auto-downgrade on day 7.
Cycle-aligned reset
usage_this_cycle resets on subscription.charged webhook, not calendar month.
Plan changes
Upgrades immediate + prorated. Downgrades deferred to period end.
Health + audit endpoints
GET /healthz for readiness; /v1/admin/* for superadmin operations.