Introduction v1
The PDFRender.io API lets you convert any public URL or raw HTML into a PDF, PNG, JPEG or WebP. Build reusable templates, store files in your own S3 bucket, add watermarks, password-protect documents, and control every aspect of delivery from a single endpoint.
https://pdfrender.io/api
Authentication
Every request must include your API key as a Bearer token in the Authorization header. Generate and manage keys from your API Keys page. Keep keys secret — never expose them in client-side code.
Authorization: Bearer sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
GET Ping
Verify your API key is valid and check your current usage and plan limits.
curl -X GET \
https://pdfrender.io/api/ping \
-H "Authorization: Bearer YOUR_API_KEY"
{
"ok": true,
"message": "pong",
"plan": "medium",
"calls_used": 142,
"calls_limit": 5000,
"formats": ["pdf", "png", "jpeg", "webp"],
"version": "v1"
}GET Me
Returns the account details associated with the API key.
curl -X GET \
https://pdfrender.io/api/me \
-H "Authorization: Bearer YOUR_API_KEY"
{
"ok": true,
"user": {
"id": 1,
"email": "you@example.com",
"full_name": "Joe Blogs",
"plan": "medium",
"api_call_count": 142,
"created_at": "2026-01-01 09:00:00"
}
}POST Generate
Generate a PDF, PNG, JPEG or WebP from a URL, raw HTML, or a saved template. Control page layout, output format, watermarks, password protection, and where the file is delivered.
{
"source": {
"url": "https://example.com", // — or —
"html": "<html>...</html>", // — or —
"template_id": 1, // provide exactly one
"data": { }, // required with template_id or html variables
"baseUrl": "https://yourdomain.com" // optional, for relative assets in html mode
},
"format": "pdf", // pdf | png | jpeg | webp
"options": {
"page": {
"size": "A4", // named size — ignored if width/height set
"width": "80mm", // custom width (overrides size)
"height": "50mm", // custom height (overrides size)
"landscape": false,
"margin": "15mm", // sets all four sides
"marginTop": "20mm", // overrides margin for one side
"marginRight": "15mm",
"marginBottom": "20mm",
"marginLeft": "15mm",
"scale": 1.0,
"ranges": "", // e.g. "1-3" or "1,3,5"
"preferCSSPageSize":false
},
"render": {
"printBackground": true,
"waitFor": "#selector", // wait for element before capture
"timeout": 30000 // ms
},
"watermark": {
"enabled": false,
"text": "CONFIDENTIAL",
"placement": "center"
},
"screenshot": {
"fullPage": true,
"width": 1280,
"height": 800,
"quality": 90, // jpeg only
"clip": { "x": 0, "y": 0, "width": 1200, "height": 630 }
},
"security": {
"password": "user-password", // Medium+ plan
"ownerPassword": "owner-password"
}
},
"delivery": {
"returnType": "json", // json | binary
"storage": "none", // none | platform | own
"fileName": "my-document",
"path": "invoices/2026/", // S3 path prefix
"bucket": "my-specific-bucket", // S3 bucket
"overwrite": false
}
}Provide exactly one of url, html, or template_id. Providing more than one will return a 422 error.
| Field | Type | Description |
|---|---|---|
| url | string | A fully qualified, publicly accessible URL to render. |
| html | string | Raw HTML string. Max 2MB. Supports {{variable}} injection when data is also provided. |
| template_id | integer | ID of a saved template from your dashboard. Requires data. Large plan+. |
| data | object | Key-value object of variable values to inject. Required with template_id, optional with html. |
| baseUrl | string | Base URL for resolving relative assets in HTML mode (images, CSS, fonts). |
| Value | Output | Notes |
|---|---|---|
| PDF document | Default. All page options apply. | |
| png | PNG image | Screenshot mode. Page options ignored. Screenshot options apply. |
| jpeg | JPEG image | Screenshot mode. quality option applies. |
| webp | WebP image | Screenshot mode. |
| Field | Type | Default | Description |
|---|---|---|---|
| size | string | A4 | Named paper size. One of: A4, A3, A5, Letter, Legal, Tabloid. Ignored if width and height are set. |
| width | string | — | Custom page width. Accepts CSS units: 80mm, 3.14in, 595px. Use with height to define a custom page size — overrides size. |
| height | string | — | Custom page height. Accepts CSS units: 50mm, 2in, 842px. Use with width to define a custom page size — overrides size. |
| landscape | boolean | false | Render in landscape orientation. Applies to named size only — not applicable when using custom width / height. |
| margin | string | 0mm | Sets all four margins. e.g. 15mm, 1in. |
| marginTop / Right / Bottom / Left | string | — | Override individual margins. Takes precedence over margin. |
| scale | float | 1.0 | Page scale between 0.1 and 2.0. |
| ranges | string | "" | Page ranges to render. e.g. 1-3 or 1,3,5. Empty = all pages. |
| preferCSSPageSize | boolean | false | Use the page size defined in the page's CSS @page rule. |
| Field | Type | Default | Description |
|---|---|---|---|
| printBackground | boolean | true | Print background colours and images. |
| waitFor | string | null | CSS selector to wait for before capture. Useful for pages with async/JS-rendered content. e.g. #chart-loaded |
| timeout | integer | 30000 | Max wait time in milliseconds. Applies to both page load and waitFor. |
| Field | Type | Default | Description |
|---|---|---|---|
| enabled | boolean | false | Enable watermark overlay. |
| text | string | "" | Watermark text. e.g. CONFIDENTIAL, DRAFT, VOID. |
| placement | string | center | One of: center, top-left, top-right, bottom-left, bottom-right, diagonal-full. |
| Field | Type | Default | Description |
|---|---|---|---|
| fullPage | boolean | true | Capture the full scrollable page. Set false for viewport only. |
| width | integer | 1280 | Viewport width in pixels. |
| height | integer | 800 | Viewport height in pixels. |
| quality | integer | 90 | JPEG quality 1–100. Ignored for PNG and WebP. |
| clip | object | null | Capture a specific region. Object with x, y, width, height in pixels. Overrides fullPage. |
Password-protect the generated PDF using 256-bit AES encryption. The user must enter the password to open the file.
| Field | Type | Default | Description |
|---|---|---|---|
| password | string | null | Password required to open the PDF. |
| ownerPassword | string | null | Owner password for edit/print permissions. Defaults to password if not set. |
Generate a Factur-X / ZUGFeRD hybrid PDF — a human-readable PDF with a machine-readable EN 16931 XML invoice embedded inside. Required for EU e-invoicing mandates (Germany Jan 2025, France Sep 2026, Belgium Jan 2026 and more). Also supports PDF/A-3 conversion without XML embedding.
| Field | Type | Default | Description |
|---|---|---|---|
| mode | string | none |
none — standard PDF (default). pdfa3 — PDF/A-3B conversion only, no XML. en16931 — Factur-X hybrid PDF with EN 16931 XML embedded. extended — Factur-X hybrid with EXTENDED profile XML. |
| profile | string | en16931 | XML conformance profile. en16931 or extended. Inferred from mode if not set. |
| xml | string | null | Optional. Your own pre-built Factur-X XML as a base64 string. When omitted the XML is auto-generated from source.data. |
| Field | Type | Default | Description |
|---|---|---|---|
| returnType | string | json | json returns metadata. binary streams the raw file bytes. |
| storage | string | none |
none — no storage (default). platform — store in platform CDN. Medium+ plan. own — store in your verified S3 bucket. Medium+ plan. |
| fileName | string | conversion-{id} | Output filename without extension. Alphanumeric, hyphens and underscores only. |
| path | string | "" | Path prefix within your S3 bucket. e.g. invoices/2026/. Trailing slash required. |
| bucket | string | "" | Override the bucket name set in Settings on a per-request basis. Useful when writing to multiple buckets with the same credentials. Only applies when storage: "own". |
| overwrite | boolean | false | When false and a file with the same name exists, the conversion ID is auto-appended to keep the filename unique. |
curl -X POST \
https://pdfrender.io/api/generate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": { "url": "https://example.com" },
"format": "pdf",
"options": {
"page": { "size": "A4", "margin": "15mm" },
"render": { "printBackground": true }
},
"delivery": { "returnType": "binary" }
}' \
--output document.pdf
Responses
All responses are JSON unless delivery.returnType is set to binary, in which case the raw file bytes are streamed directly.
{
"ok": true,
"conversion_id": 245090,
"format": "pdf",
"file_name": "invoice-2026-001.pdf",
"file_size": 84231,
"stored": true,
"cdn_url": "https://your-bucket.sfo3.cdn.digitaloceanspaces.com/invoices/2026/invoice-2026-001.pdf",
"storage": "customer_bucket",
"overwritten": false,
"auto_suffixed": false,
"return_type": "json",
"password_protected":false,
"template_id": 1,
"template_name": "Simple Invoice"
}Content-Type: application/pdf Content-Disposition: attachment; filename="invoice-2026-001.pdf" Content-Length: 84231 X-Conversion-Id: 245090 X-CDN-URL: https://... (only present if stored)
| Field | Type | Description |
|---|---|---|
| ok | boolean | Always true on success. |
| conversion_id | integer | Unique ID for this conversion. Reference for support. |
| format | string | Output format used: pdf, png, jpeg or webp. |
| file_name | string | Final filename including extension. |
| file_size | integer | File size in bytes. |
| stored | boolean | Whether the file was stored in a bucket. |
| cdn_url | string|null | Public CDN URL if stored, null otherwise. |
| storage | string|null | platform_bucket, customer_bucket, or null. |
| overwritten | boolean | True if an existing file was overwritten. |
| auto_suffixed | boolean | True if the conversion ID was appended to avoid a filename conflict. |
| password_protected | boolean | True if a password was applied to the PDF. |
| warning | string | Present if the file generated successfully but storage failed. |
Error Codes
All errors return JSON with an error field describing what went wrong.
| Status | Code | Cause |
|---|---|---|
| 401 | Unauthorized | Missing or invalid API key. |
| 403 | Forbidden | Feature requires a higher plan (platform storage, password, templates, Factur-X compliance). |
| 404 | Not Found | Template ID not found or doesn't belong to your account. |
| 422 | Unprocessable | Invalid or missing parameters, or missing mandatory Factur-X fields. Check the error and missing fields for details. |
| 429 | Too Many Requests | Per-minute rate limit exceeded. Check Retry-After header. |
| 503 | Service Unavailable | Generation server queue full or request timed out. Retry with backoff. |
{
"error": "Missing required template variables.",
"missing_fields": ["invoice_number", "lines"],
"template_id": 1,
"template_name": "Simple Invoice"
}{
"error": "Rate limit exceeded. Too many requests per minute.",
"limit": 5,
"plan": "small",
"retry_after": 47,
"upgrade_url": "https://yourdomain.com/app/settings/?tab=billing"
}{
"error": "Server is busy. Please retry in a moment.",
"code": "QUEUE_FULL",
"queue_size": 50,
"retry_after": 30
}Platform CDN Medium+ plan
Store generated files on our managed CDN and receive a permanent public URL in the response. Files are stored in your account's private folder and served over HTTPS.
curl -X POST \
https://pdfrender.io/api/generate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": { "url": "https://example.com" },
"format": "pdf",
"delivery": {
"returnType": "json",
"storage": "platform",
"fileName": "contract-001",
"overwrite": false
}
}'
Own S3 Bucket Medium+ plan
Send generated files directly to your own S3-compatible bucket (AWS, DigitalOcean Spaces, Cloudflare R2 etc.). Configure and verify your bucket once in Settings → Storage, then pass delivery.storage: "own" on any request.
curl -X POST \
https://pdfrender.io/api/generate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": { "url": "https://example.com/invoice/1001" },
"format": "pdf",
"delivery": {
"returnType": "json",
"storage": "own",
"fileName": "invoice-1001",
"path": "invoices/2026/april/",
"overwrite": false
}
}'
Use the optional bucket field to override the default bucket set in Settings on a per-request basis. All requests use the same credentials — just the destination bucket changes.
## Default bucket (from Settings) curl -X POST https://pdfrender.io/api/generate \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": { "url": "https://example.com/invoice/1001" }, "format": "pdf", "delivery": { "returnType": "json", "storage": "own", "fileName": "invoice-1001.pdf" } }' ## Override to a different bucket curl -X POST https://pdfrender.io/api/generate \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "source": { "url": "https://example.com/invoice/1001" }, "format": "pdf", "delivery": { "returnType": "json", "storage": "own", "fileName": "invoice-1001.pdf", "bucket": "my-invoices-bucket" } }'
| Scenario | Resulting path |
|---|---|
| No path set | pdfs/{year}/{month}/invoice-1001.pdf |
| path: "invoices/2026/" | invoices/2026/invoice-1001.pdf |
| bucket: "other-bucket" | other-bucket/pdfs/{year}/{month}/invoice-1001.pdf |
| Conflict + overwrite: false | invoices/2026/invoice-1001_245091.pdf |
| Conflict + overwrite: true | invoices/2026/invoice-1001.pdf |
Factur-X & ZUGFeRD Compliance Large+ plan
Generate hybrid PDFs that are both human-readable and machine-readable — a standard visual PDF with a structured EN 16931 XML invoice embedded inside. Required for EU e-invoicing mandates across Germany, France, Belgium, Poland and more. Compatible with all major ERP systems including Microsoft Dynamics 365 Business Central, SAP, and Sage.
French/EU standard. Same XML schema as ZUGFeRD. Accepted across all EU markets.
German standard. Identical XML to Factur-X. Accepted by DATEV, SAP, Lexware.
The underlying EU semantic data model. Both Factur-X and ZUGFeRD comply with this standard.
| Mode | Output | XML embedded | Plan |
|---|---|---|---|
| none | Standard PDF | No | All plans |
| pdfa3 | PDF/A-3B compliant PDF | No | Large+ |
| en16931 | Factur-X hybrid PDF/A-3 | Yes — EN 16931 | Large+ |
| extended | Factur-X hybrid PDF/A-3 | Yes — EXTENDED | Large+ |
These fields must be present in source.data when using mode: en16931 or mode: extended. Missing fields cause a hard 422 failure listing exactly which are absent.
| Field | Type | Example | Notes |
|---|---|---|---|
| invoice_number | string | "INV-2026-001" | Unique document identifier |
| invoice_date | string | "2026-04-05" | ISO date or any parseable format |
| currency | string | "GBP" | ISO 4217 currency code |
| tax_rate | number | 20 | Default VAT rate as a percentage |
| seller_name | string | "Acme Ltd" | |
| seller_country | string | "GB" | ISO 3166-1 alpha-2 |
| seller_vat | string | "GB123456789" | VAT registration number |
| buyer_name | string | "Customer Corp" | |
| buyer_country | string | "DE" | ISO 3166-1 alpha-2 |
| lines | array | [{...}] | Each item needs description, quantity, unit_price |
| Field | Description |
|---|---|
| seller_address, seller_city, seller_postcode | Full seller postal address. Required for EXTENDED profile. |
| seller_email, seller_phone | Seller contact details |
| seller_trading_name | Trading name if different from legal name |
| seller_tax_number | Seller tax / fiscal number (FC scheme) |
| seller_id, seller_global_id | Internal or GLN identifier for the seller party |
| buyer_vat | Required for B2B cross-border EU transactions |
| buyer_address, buyer_city, buyer_postcode | Full buyer postal address. Required for EXTENDED profile. |
| buyer_email, buyer_phone | Buyer contact details |
| buyer_id, buyer_global_id | Internal or GLN identifier for the buyer party |
| buyer_reference | Buyer accounting or routing reference |
| payment_due_date | Due date for payment — strongly recommended. Required for EXTENDED. |
| iban, bic | Seller bank account for SEPA credit transfer (payment means type 58) |
| account_name | Bank account holder name (EXTENDED) |
| creditor_reference | SEPA direct debit creditor reference (EXTENDED) |
| payment_reference | Payment reference / remittance info |
| payment_terms | Human-readable payment terms text |
| order_number | Buyer purchase order reference |
| seller_order_number | Seller's own order/sales order number (EXTENDED) |
| contract_number | Contract reference number |
| project_number, project_name | Project reference for procurement tracking (EXTENDED) |
| delivery_date | Actual delivery date at header level |
| invoice_period_start, invoice_period_end | Billing period start and end dates (EXTENDED) |
| tax_point_date | VAT tax point date if different from invoice date (EXTENDED) |
| tax_currency | Tax reporting currency if different from invoice currency (EXTENDED) |
| tax_exemption_reason | Reason text for zero-rated or exempt VAT |
| notes | Invoice footer note or general annotation |
| document_type | invoice (default) or credit_note — sets TypeCode to 380 or 381 |
| document_name | Custom document title |
| prepaid_amount | Amount already paid — deducted from DuePayableAmount |
| amount_due | Override the calculated due amount |
| rounding_amount | Rounding adjustment applied to grand total (EXTENDED) |
| accounting_reference | Header-level GL account code (EXTENDED) |
| business_process | Business process identifier in document context (EXTENDED) |
| shipto_name, shipto_address, shipto_city, shipto_country | Ship-to delivery party (EXTENDED) |
| despatch_advice_number | Despatch advice document reference (EXTENDED) |
| receiving_advice_number | Receiving advice document reference (EXTENDED) |
| card_last4, card_holder_name | Card payment means details (EXTENDED, type 48) |
| allowances[] | Header-level discounts. Each: { amount, reason, reason_code, tax_rate, percentage } (EXTENDED) |
| charges[] | Header-level surcharges. Each: { amount, reason, reason_code, tax_rate } (EXTENDED) |
| lines[].tax_rate | Per-line VAT rate — overrides top-level tax_rate |
| lines[].unit | UN/ECE unit code e.g. C62 (piece), HUR (hour), KGM (kg) |
| lines[].sku | Seller-assigned product/SKU identifier |
| lines[].buyer_ref | Buyer-assigned product reference |
| lines[].global_id | GTIN or other global product identifier (EXTENDED) |
| lines[].description_long | Extended product description |
| lines[].origin_country | Country of origin for the product (EXTENDED) |
| lines[].gross_price | Gross unit price before discount (EXTENDED) |
| lines[].discount | Unit price discount amount from gross to net (EXTENDED) |
| lines[].delivery_date | Per-line delivery date (EXTENDED) |
| lines[].buyer_order_ref | Buyer order line reference |
| lines[].note | Per-line note or annotation |
| lines[].allowances[] | Per-line allowances. Each: { amount, reason } (EXTENDED) |
| lines[].line_reference | GL account code for this line (EXTENDED) |
curl -X POST \
https://pdfrender.io/api/generate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": {
"template_id": 1,
"data": {
"invoice_number": "INV-2026-001",
"invoice_date": "2026-04-05",
"payment_due_date": "2026-05-05",
"currency": "GBP",
"tax_rate": 20,
"seller_name": "Acme Ltd",
"seller_address": "123 Main Street",
"seller_city": "London",
"seller_postcode": "EC1A 1BB",
"seller_country": "GB",
"seller_vat": "GB123456789",
"seller_email": "billing@acme.com",
"iban": "GB29NWBK60161331926819",
"bic": "NWBKGB2L",
"buyer_name": "Customer Corp",
"buyer_address": "456 High Street",
"buyer_city": "Manchester",
"buyer_postcode": "M1 1AA",
"buyer_country": "GB",
"buyer_vat": "GB987654321",
"lines": [
{ "description": "Product A", "quantity": 10, "unit_price": 49.99 },
{ "description": "Service B", "quantity": 2, "unit_price": 150.00 }
],
"notes": "Payment due within 30 days."
}
},
"format": "pdf",
"options": {
"page": { "size": "A4", "margin": "15mm" },
"render": { "printBackground": true },
"compliance": { "mode": "en16931" }
},
"delivery": {
"returnType": "binary",
"fileName": "INV-2026-001"
}
}' \
--output invoice-facturx.pdf
{
"error": "Missing mandatory fields for EN16931 compliance.",
"missing": ["seller_vat", "buyer_country", "payment_due_date"],
"compliance": "en16931",
"help": "Ensure all mandatory fields for EN16931 are present in source.data."
}| Country | B2G | B2B — receive | B2B — send |
|---|---|---|---|
| Germany | 2020 | Jan 2025 | Jan 2027 |
| France | 2020 | Sep 2026 | Sep 2027 (SME) |
| Belgium | Active | Jan 2026 | Jan 2026 |
| Poland | Active | Feb 2026 | Feb 2026 |
| Italy | Active | Active | Active |
| Romania | Active | Jan 2024 | Jul 2024 |
| Spain | 2015 | 2025 | 2025 |
| UK | — | Voluntary | Voluntary (PEPPOL) |
POST Validate PDF Large+ plan
Validate any PDF for Factur-X / ZUGFeRD and PDF/A-3 structural compliance. Pass either a public HTTPS URL or a base64-encoded PDF. Returns a detailed report of which compliance rules passed and failed.
| Field | Type | Required | Description |
|---|---|---|---|
| pdf_base64 | string | Optional* | The PDF file as a base64-encoded string. Max 20MB. |
| url | string | Optional* | A public https:// URL to fetch and validate. Must return a PDF. |
| profile | string | Optional |
Compliance profile to validate against. en16931 — Factur-X EN 16931 (default) extended — Factur-X EXTENDED pdfa3 — PDF/A-3B structure only |
* Provide either pdf_base64 or url — not both.
# Validate a local PDF file
curl -X POST \
https://pdfrender.io/api/validate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"pdf_base64\": \"$(base64 -i invoice.pdf)\",
\"profile\": \"en16931\"
}"
{
"ok": true,
"compliant": true,
"profile": "en16931",
"file_size": 18547,
"page_count": 1,
"rules_passed": 9,
"rules_failed": 0,
"passed": [
"PDF/A-3 OutputIntent present",
"XMP Metadata stream present",
"PDF/A-3 XMP identifier present",
"EmbeddedFiles present: factur-x.xml",
"AF array present in document catalog",
"AFRelationship = /Alternative found",
"factur-x.xml found in EmbeddedFiles",
"Factur-X XMP present — level: EN 16931",
"PDF has 1 page(s)"
],
"failures": [],
"fx_level": "EN 16931",
"fx_filename": "factur-x.xml",
"embedded_files": ["factur-x.xml"],
"validator": "pikepdf-structural"
}{
"ok": true,
"compliant": false,
"rules_passed": 6,
"rules_failed": 3,
"passed": [ "..." ],
"failures": [
{
"rule": "factur-x.xml",
"description": "factur-x.xml not found in EmbeddedFiles. Found: none"
},
{
"rule": "AFRelationship",
"description": "No embedded file with AFRelationship=/Alternative found"
},
{
"rule": "Factur-X XMP",
"description": "Factur-X extension schema not found in XMP metadata"
}
],
"fx_level": null,
"fx_filename": null
}| Field | Type | Description |
|---|---|---|
| compliant | boolean | Whether the PDF passed all compliance checks. |
| rules_passed | integer | Number of structural rules that passed. |
| rules_failed | integer | Number of structural rules that failed. |
| passed | array | List of passing rule descriptions. |
| failures | array | List of failed rules with rule and description per item. |
| fx_level | string|null | Factur-X conformance level found in XMP. e.g. EN 16931. |
| fx_filename | string|null | Embedded XML filename declared in XMP. e.g. factur-x.xml. |
| embedded_files | array | All filenames found in the EmbeddedFiles name tree. |
| page_count | integer | Number of pages in the PDF. |
| file_size | integer | File size in bytes. |
| validator | string | Always pikepdf-structural — structural validation of PDF/A-3 markers, EmbeddedFiles, AF array and Factur-X XMP. |
Webhook Delivery Medium+ plan
Fire-and-forget PDF generation. Submit a request and receive the result via HTTP callback when the file is ready — no need to keep the connection open. Ideal for batch workflows, server-to-server integrations, and any environment where long HTTP connections are unreliable.
Your request returns HTTP 202 with a job ID in milliseconds — no waiting for rendering.
Every webhook delivery includes an X-PDFRender-Signature header so you can verify authenticity.
3 attempts with backoff (0s, 5min, 30min) if your endpoint is temporarily unavailable.
| Field | Type | Required | Description |
|---|---|---|---|
| returnType | string | Required | Set to webhook to enable async delivery. |
| webhookUrl | string | Required | Your public https:// endpoint that will receive the POST when the PDF is ready. |
| webhookSecret | string | Optional | A secret used to sign the webhook payload. If set, an X-PDFRender-Signature header is included so you can verify delivery authenticity. |
| storage | string | Required | Must be platform or own. The webhook payload includes a CDN URL — storage is required to produce one. |
| fileName | string | Optional | Output filename without extension. Included in the webhook payload. |
| path | string | Optional | Storage path prefix. e.g. invoices/2026/. Trailing slash required. |
# Fire and forget — returns job_id immediately
curl -X POST \
https://pdfrender.io/api/generate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": {
"template_id": 1,
"data": {
"invoice_number": "INV-2026-001",
"customer_name": "Acme Corp"
}
},
"format": "pdf",
"options": {
"page": { "size": "A4", "margin": "15mm" },
"render": { "printBackground": true }
},
"delivery": {
"returnType": "webhook",
"webhookUrl": "https://yourapp.com/pdf-ready",
"webhookSecret": "your-signing-secret",
"storage": "own",
"fileName": "INV-2026-001",
"path": "invoices/2026/"
}
}'
{
"ok": true,
"job_id": "job_a3f9k2x8m1",
"status": "queued",
"webhook_url": "https://yourapp.com/pdf-ready",
"message": "Job queued. Your webhook will be called when the PDF is ready.",
"status_url": "https://pdfrender.io/api/v1/jobs/job_a3f9k2x8m1"
}When the PDF is ready your endpoint receives a POST request with the following JSON body and headers.
| Header | Description |
|---|---|
| Content-Type | application/json |
| X-PDFRender-Job-Id | The job ID |
| X-PDFRender-Timestamp | Unix timestamp of delivery |
| X-PDFRender-Signature | sha256=HMAC of the request body — only present if webhookSecret was set |
{
"job_id": "job_a3f9k2x8m1",
"ok": true,
"status": "complete",
"format": "pdf",
"file_name": "INV-2026-001.pdf",
"file_size": 84231,
"cdn_url": "https://your-bucket.nyc3.cdn.digitaloceanspaces.com/invoices/2026/INV-2026-001.pdf",
"storage": "customer_bucket",
"render_ms": 2341,
"delivered_at": "2026-04-06T12:00:03+00:00"
}If you set a webhookSecret, always verify the X-PDFRender-Signature header before processing the payload. The signature is computed as sha256=HMAC-SHA256(rawBody, secret). Use a timing-safe comparison to prevent timing attacks.
$body = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_PDFRENDER_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $body, 'your-signing-secret');
if (!hash_equals($expected, $signature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
$data = json_decode($body, true);
// PDF is ready at: $data['cdn_url']
| Attempt | Delay after failure | Notes |
|---|---|---|
| 1 | Immediate | First attempt — next available cron run |
| 2 | 5 minutes | Triggered if attempt 1 returns non-2xx or times out |
| 3 | 30 minutes | Final attempt — job marked failed if this also fails |
GET Job Status Medium+ plan
Poll for the status of an async job. Useful as a fallback if your webhook endpoint missed a delivery, or to confirm completion before taking action.
curl -X GET \
https://pdfrender.io/api/jobs/job_a3f9k2x8m1 \
-H "Authorization: Bearer YOUR_API_KEY"
{
"ok": true,
"job_id": "job_a3f9k2x8m1",
"status": "complete",
"webhook_url": "https://yourapp.com/pdf-ready",
"attempts": 1,
"created_at": "2026-04-06 12:00:00",
"completed_at": "2026-04-06 12:00:03",
"expires_at": "2026-04-13 12:00:00",
"cdn_url": "https://your-bucket.nyc3.cdn.digitaloceanspaces.com/invoices/2026/INV-2026-001.pdf",
"file_name": "INV-2026-001.pdf",
"file_size": 84231,
"render_ms": 2341,
"error": null
}| Status | Meaning |
|---|---|
| queued | Waiting to be picked up — usually less than 60 seconds |
| processing | Currently being generated by the render engine |
| complete | PDF generated, stored, and webhook delivered successfully |
| failed | All 3 delivery attempts exhausted — check the error field |
Templates Large+ plan
Build reusable PDF layouts once in the visual template builder, declare your variables, and generate personalised documents on demand by passing a JSON data payload. No HTML required on your side.
Design your layout in the drag-and-drop builder
Declare variables — strings, numbers, arrays
POST with template_id and a data object
Get back a PDF — same as any other generate call
Template Builder
The builder is available at Templates in your dashboard. It uses a section → row → column → block model.
Drag layout blocks (1 col, 2 col, 60/40 etc.) onto the canvas, then drag content blocks (Text, Table, Totals, Image, QR Code, HTML) into columns.
Click any block to edit its properties — font, colour, alignment, table columns, computed formulas. Switch to Variables to manage variables. Switch to Page for paper size and footer.
The builder saves 3 seconds after your last change. Click Save at any time. Use Preview to see the template rendered with sample data.
Block Types
| Block | Purpose | Variable support |
|---|---|---|
| text | Paragraph, heading, label or any static or dynamic text | ✓ {{variable}} |
| table | Repeating rows from an array variable | ✓ dataKey maps to array |
| totals | Auto-computed subtotal, tax and grand total rows | ✓ computed expressions |
| image | Static or dynamic image from a URL or variable | ✓ {{logo_url}} |
| divider | Horizontal separator line | — |
| spacer | Empty vertical space | — |
| qrcode | QR code from a value or variable | ✓ {{booking_ref}} |
| html | Raw HTML block for complex layouts | ✓ {{variable}} in HTML |
Variables
Declare variables in the Variables tab of the builder. Use {{variable_key}} inside Text or HTML blocks. The value is injected at render time from your source.data object.
| Type | Usage | Example value | Example in template |
|---|---|---|---|
| string | Names, dates, addresses, notes | "Joe Blogs" | {{customer_name}} |
| number | Used in computed expressions | 20 | {{tax_rate}}% or in: subtotal * (tax_rate / 100) |
| array | Repeating rows for Table and Totals blocks | [{description, quantity, unit_price}] | Table dataKey: "lines" |
Template API Usage
Find your template ID in the builder URL: /app/templates/builder/?id=12. Pass it as source.template_id alongside your data.
curl -X POST \
https://pdfrender.io/api/generate \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"source": {
"template_id": 1,
"data": {
"company_name": "Acme Ltd",
"company_email": "billing@acme.com",
"company_address": "123 Main Street, London",
"invoice_number": "INV-2026-001",
"invoice_date": "3 April 2026",
"due_date": "3 May 2026",
"customer_name": "Joe Blogs",
"customer_email": "josh@example.com",
"tax_rate": 20,
"notes": "Thank you for your business.",
"lines": [
{ "description": "LED Strip EF160", "quantity": 10, "unit_price": 49.99 },
{ "description": "Installation", "quantity": 2, "unit_price": 150.00 }
]
}
},
"format": "pdf",
"delivery": { "returnType": "binary", "fileName": "invoice-INV-2026-001" }
}' \
--output invoice.pdf
Starter Templates
Your account includes ready-made templates. Duplicate and customise them in the builder, or use them as-is straight away.
| ID | Name | Category | Key variables |
|---|---|---|---|
| 1 | Simple Invoice | invoice | company_name, customer_name, invoice_number, lines[], tax_rate |
| 2 | Simple Receipt | receipt | company_name, receipt_number, items[], tax_rate, payment_method |
| 3 | Formal Letter | letter | sender_name, recipient_name, subject, body_paragraph_1 |
| 4 | Boarding Pass | ticket | airline_name, flight_number, departure_code, arrival_code, passenger_name, seat, booking_ref |
| 5 | Business Report | report | company_name, report_title, executive_summary, rows[], metric_1_label, metric_1_value |
| 6 | Purchase Order | contract | company_name, po_number, vendor_name, items[], tax_rate |
Plan Limits
All plans include the core generate endpoint. Higher plans unlock storage, password protection, templates and priority queue processing.
| Plan | Calls / mo | Rate / min | Priority | CDN Storage | Own S3 | Password | Templates | Factur-X |
|---|---|---|---|---|---|---|---|---|
| Free | 100 | 2 | 0 | ✕ | ✕ | ✕ | ✕ | ✕ |
| Small | 1,500 | 5 | 1 | ✕ | ✕ | ✕ | ✕ | ✕ |
| Medium | 5,000 | 15 | 2 | ✓ | ✓ | ✓ | ✕ | ✕ |
| Large | 25,000 | 30 | 3 | ✓ | ✓ | ✓ | ✓ | ✓ |
| xLarge | 50,000 | 60 | 4 | ✓ | ✓ | ✓ | ✓ | ✓ |
| Enterprise | Custom | 120 | 5 | ✓ | ✓ | ✓ | ✓ | ✓ |
Rate Limits
In addition to monthly call limits, each plan has a per-minute rate limit. Exceeding it returns a 429 response with a Retry-After header indicating how many seconds to wait before retrying.
{
"error": "Rate limit exceeded. Too many requests per minute.",
"limit": 5,
"plan": "small",
"retry_after": 47,
"upgrade_url": "https://yourdomain.com/app/settings/?tab=billing"
}Usage Notifications
We send email alerts when your monthly API call usage reaches 50%, 80% and 100% of your plan limit. This gives you time to upgrade before hitting your limit mid-month.
First warning — plenty of headroom remaining
Second warning — consider upgrading soon
Final warning — API calls will be rejected until reset or upgrade