API docs
Send HTML to one endpoint and get a PDF back. Requests use JSON, and a successful response is the PDF itself.
Quick start
Put your API key in the Authorization header. Each key is shown once, so keep it somewhere safe, and only use it from your server.
curl https://api.pagedpdf.com/v1/pdf \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"html": "<h1>Invoice 1042</h1>", "footer": "Page {page} of {pages}"}' \
-o invoice.pdf
Windows
Windows shells handle quotes differently, so use these one-line versions.
PowerShell:
curl.exe https://api.pagedpdf.com/v1/pdf -H "Authorization: Bearer YOUR_KEY" -H "Content-Type: application/json" -d '{\"html\": \"<h1>Invoice 1042</h1>\"}' -o invoice.pdf
Command Prompt:
curl https://api.pagedpdf.com/v1/pdf -H "Authorization: Bearer YOUR_KEY" -H "Content-Type: application/json" -d "{""html"": ""<h1>Invoice 1042</h1>""}" -o invoice.pdf
Node.js
import { writeFile } from 'node:fs/promises';
const response = await fetch('https://api.pagedpdf.com/v1/pdf', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAGEDPDF_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html: '<h1>Invoice 1042</h1>',
footer: 'Page {page} of {pages}',
}),
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message}`);
}
await writeFile('invoice.pdf', Buffer.from(await response.arrayBuffer()));
Python
import os
import requests
response = requests.post(
"https://api.pagedpdf.com/v1/pdf",
headers={"Authorization": f"Bearer {os.environ['PAGEDPDF_KEY']}"},
json={"html": "<h1>Invoice 1042</h1>", "footer": "Page {page} of {pages}"},
timeout=90,
)
if not response.ok:
error = response.json()["error"]
raise RuntimeError(f"{error['code']}: {error['message']}")
with open("invoice.pdf", "wb") as pdf:
pdf.write(response.content)
PHP
<?php
$request = curl_init('https://api.pagedpdf.com/v1/pdf');
curl_setopt_array($request, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 90,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('PAGEDPDF_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'html' => '<h1>Invoice 1042</h1>',
'footer' => 'Page {page} of {pages}',
]),
]);
$body = curl_exec($request);
$status = curl_getinfo($request, CURLINFO_HTTP_CODE);
if ($status !== 200) {
$error = json_decode($body)->error;
throw new RuntimeException("{$error->code}: {$error->message}");
}
file_put_contents('invoice.pdf', $body);
Options
Send these in the JSON body of POST /v1/pdf. Only html is required, and an unknown option returns an error.
| Option | Default | What it does |
|---|---|---|
html |
Required | The document. The whole JSON body can be up to 20 MB. |
base_url |
None | Relative URLs resolve against this, and it counts as the page's origin, so same-origin fonts need no CORS headers. |
format |
A4 |
A3, A4, A5, Legal, Letter or Tabloid. A CSS @page size takes priority. |
landscape |
false |
Turns the page sideways. |
margin |
15mm top and bottom, 12mm sides | A length, or an object with any of top, right, bottom and left. Lengths use mm, cm, in or px. |
header |
None | Plain text at the top of each page, up to 500 characters. |
footer |
None | Plain text at the bottom of each page, up to 500 characters. |
javascript |
false |
Runs the document's scripts, then waits for the network to go quiet, for up to 3 seconds. |
table_fixes |
true |
Keeps table rows whole, repeats table headers and keeps headings with what follows. |
Headers and footers
Use {page} for the page number and {pages} for the total. A header or footer raises its margin to at least 15mm, so it can't land on your content.
{ "html": "<h1>Report</h1>", "footer": "Acme Ltd, page {page} of {pages}" }
Page size and margins
{ "html": "<h1>Report</h1>", "format": "Letter", "landscape": true, "margin": { "top": "1in", "left": "20mm" } }
Sides you leave out keep their default. "margin": 0 removes all margins.
Tables
With table_fixes on, table header and footer rows repeat on every page, even if your CSS sets them to display: block. Your own CSS can still override the row and heading rules, for example to let a very tall row split:
tr { break-inside: auto; }
Fonts and images
These fonts are installed, so you can name them without loading anything: Liberation Sans, Serif and Mono, which stand in for Arial, Times New Roman and Courier New, plus Noto, Inter, Lato, Open Sans and Roboto. Emoji and Chinese, Japanese and Korean text work without extra setup.
For your own fonts and images, use a public URL or a data: URI. Relative URLs need base_url:
{ "html": "<img src=\"/logo.png\">", "base_url": "https://www.example.com/" }
The renderer can only reach public addresses on ports 80 and 443. That means localhost, private networks and file:// links won't load. If your images are on a private network, use data: URIs instead.
A document also can't navigate away from itself. The original document is what gets printed.
Scripts
Scripts are off by default, so a page that builds its content with JavaScript comes out blank until you set "javascript": true. A page that keeps polling still prints after the 3 second wait.
Warnings
A PDF can succeed with something missing, so every response says what didn't load. X-PagedPDF-Warnings is a JSON list of up to 10 problems, and X-PagedPDF-Warning-Count is the total.
[{"code":"relative_url","url":"/logo.png","message":"This relative URL has nothing to resolve against. Set base_url or use an absolute URL."}]
| Code | Meaning |
|---|---|
relative_url |
A relative URL with no base_url to resolve against. |
load_failed |
A resource failed to load, or the address was refused. |
font_failed |
A web font couldn't be used, so a fallback font was printed. |
image_failed |
An image couldn't be displayed. |
blocked |
A URL used something other than http or https. |
navigation_blocked |
The page tried to navigate away. |
Response headers
| Header | Meaning |
|---|---|
X-PagedPDF-Pages |
Page count. |
X-PagedPDF-Used |
Documents used this month, including this one. |
X-PagedPDF-Limit |
Your monthly limit. |
X-PagedPDF-Warnings |
JSON list of up to 10 problems. |
X-PagedPDF-Warning-Count |
Total number of problems. |
X-PagedPDF-Render-Ms |
Render time in milliseconds. |
Errors
Errors are JSON, and only successful PDFs count towards your limit.
{"error":{"code":"quota_exceeded","message":"You've used all 2000 documents for this month."}}
| Status | Code | What to do |
|---|---|---|
| 400 | invalid_request, invalid_json |
Fix the request. The message says what's wrong. |
| 401 | unauthorized |
Check your key. |
| 404 | not_found |
The only endpoint is POST /v1/pdf. |
| 405 | method_not_allowed |
Use POST. |
| 413 | body_too_large |
Keep the request under 20 MB. Move large images to URLs. |
| 422 | pdf_too_large |
The PDF is over 50 MB. Use smaller images or split the document. |
| 429 | quota_exceeded |
You've reached your monthly limit. Upgrade, or wait until the 1st. |
| 429 | rate_limited |
Slow down, and retry after the Retry-After header. |
| 503 | busy, render_unavailable |
Retry after the Retry-After header. |
| 504 | render_timeout |
The render took over 30 seconds. The message names up to 3 URLs it was still waiting for. |
Limits
| Limit | Value |
|---|---|
| Request size | 20 MB |
| PDF size | 50 MB |
| Render time | 30 seconds |
| Requests | 120 a minute per key |
| Monthly documents | Set by your plan, reset on the 1st, UTC |
A busy moment can add queue time, so set your HTTP client's timeout to at least 90 seconds.
Your documents
Each job gets its own browser process, which is deleted when the job finishes, so documents never share cookies, storage or memory. The privacy page explains what passes through the service and what's stored.