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.