PagePenny

Docs

Base URL: https://api.pagepenny.com. JSON in, JSON or a file out. Every error has the same shape.

Quickstart

Get a free key, then render the sample invoice:

curl https://api.pagepenny.com/v1/render \
  -H "Authorization: Bearer pp_live_..." \
  -H "Content-Type: application/json" \
  -d '{"template_id": "tpl_invoice_classic",
       "data": {"seller": {"name": "Northwind"},
                "customer": {"name": "Acme Corp"},
                "invoice": {"number": "INV-1", "date": "2026-10-15"},
                "items": [{"description": "Design", "quantity": 1, "unit_price": 2400}]}}'

The response has a download link that works for 24 hours by default:

{
  "id": "rnd_01m51...",
  "status": "succeeded",
  "format": "pdf",
  "pages": 1,
  "bytes": 111340,
  "duration_ms": 312,
  "warnings": [],
  "url": "https://api.pagepenny.com/f/rnd_01m51...?exp=...&sig=...",
  "expires_at": "2026-10-17T03:08:00+00:00"
}

Authentication

Send your key as Authorization: Bearer pp_live_.... Keys are shown once when created. Make and revoke them in your dashboard. Keep them on your server, not in browser code.

POST /v1/render

Send exactly one of template_id or html. Everything else is optional.

FieldWhat it does
template_idA starter template, e.g. tpl_invoice_classic.
htmlYour own HTML. {{ placeholders }}, loops and filters work (Jinja). Up to 2 MB.
dataThe JSON your template uses. Up to 1 MB.
formatpdf, png, jpeg or webp. Defaults to the template's format, or pdf.
pdf{ page_size: Letter|A4|..., landscape, margin: "12mm", scale }
image{ width, height, scale, quality, full_page }. Up to 4096 px a side.
outputurl (default), binary (the raw file, up to 4 MB), or base64.
asynctrue queues the render. Use it for long documents (up to 500 pages).
webhook_urlWith async: an https URL we POST to when the render finishes.
expires_inDownload link lifetime in seconds. Default 86400, max 604800.

Templates can use {{ amount | money('USD') }}, {{ when | date('medium') }} and {{ x | number(2) }}. Missing values render blank and come back as a missing_variable warning. JavaScript does not run. Images, fonts and stylesheets can load from public https URLs.

Send an Idempotency-Key header to retry safely: the same key and body returns the original result and is charged once.

Templates

GET /v1/templates lists them. GET /v1/templates/{id} returns sample data, which is the easiest way to see what each one expects. Starters: tpl_invoice_classic, tpl_invoice_modern, tpl_receipt_80mm, tpl_quote, tpl_report_paged, tpl_certificate, tpl_og_basic, tpl_og_blog, tpl_social_square.

Async and webhooks

With "async": true you get 202 and an id right away. Poll GET /v1/renders/{id} or pass webhook_url. Webhooks are signed with your account's secret (in the dashboard):

X-PagePenny-Signature: t=1760580000,v1=<hex HMAC-SHA256 of "t.<raw body>">

# Python
import hmac, hashlib
def valid(secret, header, body: bytes):
    t, v1 = (p.split("=", 1)[1] for p in header.split(","))
    mac = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(mac, v1)

n8n, Make, Zapier

Use each tool's HTTP request module: method POST, URL https://api.pagepenny.com/v1/render, header Authorization: Bearer pp_live_..., JSON body as in the quickstart. To attach the PDF to an email or upload it somewhere, set "output": "binary" and have the module return a file. To just store a link, keep the default and use url from the response.

MCP for AI agents

PagePenny is a remote MCP server at https://api.pagepenny.com/mcp/. Tools: list_templates, get_template, render, get_render. Add it to Claude Code:

claude mcp add --transport http pagepenny https://api.pagepenny.com/mcp/ \
  --header "Authorization: Bearer pp_live_..."

Then ask for a document in plain words, e.g. "make an invoice for Acme for 3 hours of design at $90".

Pay per call (coming soon)

Not live yet. When it launches, agents with a USDC wallet on Base will be able to call POST /v1/render with no key. The first call returns 402 with payment requirements; the agent signs a $0.01 payment and retries with an X-PAYMENT header. You are charged only after the file renders.

Errors and limits

{"error": {"code": "quota_exceeded", "message": "monthly limit of 100 renders reached", "details": {...}}}
401 unauthorizedMissing or revoked key.
404 template_not_foundNo such template id.
409 idempotency_conflictSame Idempotency-Key, different body.
413 too_many_pages / input_too_largeOver 50 pages in a sync render (use async), or input over the limit.
422 invalid_request / template_errorBad body, or the template failed (line number included).
429 rate_limited / quota_exceededToo fast (see Retry-After), or the monthly limit or cap is reached.
503 capacityBusy, retry after a couple of seconds. Not charged.
504 render_timeoutTook over 20s. Retry with async.

Limits: 10 requests a minute on Free, 120 on paid. Files are kept 7 days; request data is never stored. Renders that fail because of a problem on our side are not charged.