API reference

Generate a PDF with one POST request

Send HTML, a URL or a template, get a finished PDF back. No SDK required — any HTTP client works, and the OpenAPI 3.1 spec generates one for you. The free tier covers 50 documents a month.

Quick start

Create a free account, confirm your email, and your key is waiting in the dashboard. Then one request:

# 1. create a free account and copy your key from the dashboard
#    https://pdfgeny.com/account/signup/

# 2. render a document
curl -X POST https://pdfgeny.com/api/v1/render \
  -H "Authorization: Bearer $PDFGENY_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html":"<h1>Invoice 001</h1>"}' \
  --output invoice.pdf

Authentication

Every request carries your key in a header. X-API-Key works as well if a bearer token is awkward in your client. Keys are shown once at creation and stored hashed — if you lose one, revoke it and create another in the dashboard.

Authorization: Bearer pg_your_key_here

POST /api/v1/render — synchronous

Send exactly one of html, url or template. The response is the PDF itself (Content-Type: application/pdf), or JSON with a stored document link if you pass "response": "json". Waits up to 60 s; for slow pages or bulk work use jobs.

FieldTypeDefaultNotes
htmlstring—Markup to render. Up to 2 MB.
urlstring—Public http(s) page. Private networks are refused.
templatestring—Template id from GET /v1/templates, e.g. invoice. Pair with data.
dataobject{}Field values for the template; missing fields use the template's examples.
formatstringA4A3, A4, A5, Letter, Legal, Tabloid.
landscapeboolfalse
marginstring | object12mmOne CSS length, or {"top","right","bottom","left"}.
scalenumber1.00.1 – 2.
print_backgroundbooltrue
header_html / footer_htmlstring—Repeated on every page. and are replaced.
wait_forstring—CSS selector to wait for, or milliseconds (max 15000). Useful for charts.
enginestringchromiumweasy = WeasyPrint: no JavaScript, crisp output for table-heavy documents, cheaper.
pdfaboolfalseConvert to PDF/A-2b for archiving.
width + heightstring—Exact page size with units ("1200px", "210mm"). Overrides format — useful for social images, labels and tickets.
titlestring—PDF document title metadata.
filenamestringdocument.pdfContent-Disposition and stored name.
storeboolfalseKeep the file 7 days and make it downloadable by signed URL.
responsestringbinaryjson → {document_id, document_url, size_bytes, duration_ms, expires}.

Response headers: X-Render-Duration-Ms, X-Quota-Remaining.

POST /api/v1/jobs — asynchronous

Same body as /render plus an optional webhook_url (https). Returns 202 immediately with a job id; poll GET /api/v1/jobs/{id} or wait for the webhook. Finished jobs carry document_url, valid for 7 days.

curl -X POST https://pdfgeny.com/api/v1/jobs \
  -H "Authorization: Bearer $PDFGENY_KEY" -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/report","webhook_url":"https://your.app/hooks/pdf"}'

{"id":"27015855ad29…","status":"queued","created":"2026-08-31T11:20:30Z"}

Webhook delivery

We POST {"event":"job.finished","job":{…}} with header X-PDFGeny-Signature: sha256=<HMAC-SHA256 of the raw body>, keyed with your webhook secret (GET /api/v1/webhook-secret; POST rotates it). Failed deliveries retry 5 times over ~3 hours. Verify like this:

import hmac, hashlib
expected = "sha256=" + hmac.new(SECRET.encode(), raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, request.headers["X-PDFGeny-Signature"])

POST /api/v1/batch — many documents

Up to 100 documents in one call. Top-level options apply to every item; each item may override them. Track progress at GET /api/v1/batch/{batch_id}.

{"format":"A5",
 "documents":[
   {"template":"receipt","data":{"amount":"12.00","payer":"Ada"}},
   {"template":"receipt","data":{"amount":"7.50","payer":"Grace"}},
   {"html":"<h1>Custom</h1>"}
 ]}

GET /api/v1/templates

The catalog with every field a template accepts and an example value. Render one with {"template":"invoice","data":{…}} — page size and margins come from the template unless you override them.

curl https://pdfgeny.com/api/v1/templates
{"templates":[{"id":"invoice","name":"Invoice","fields":[{"name":"client","label":"Bill to","example":"Acme Corporation"},…]},…]}

Examples

Python

import requests

pdf = requests.post(
    "https://pdfgeny.com/api/v1/render",
    headers={"Authorization": f"Bearer {KEY}"},
    json={"html": "<h1>Invoice 001</h1>", "format": "A4"},
    timeout=60,
)
open("invoice.pdf", "wb").write(pdf.content)

Node.js

const res = await fetch("https://pdfgeny.com/api/v1/render", {
  method: "POST",
  headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ url: "https://example.com/report", format: "Letter" }),
});
await writeFile("report.pdf", Buffer.from(await res.arrayBuffer()));

PHP

$ch = curl_init("https://pdfgeny.com/api/v1/render");
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ["Authorization: Bearer $key", "Content-Type: application/json"],
  CURLOPT_POSTFIELDS => json_encode(["html" => "<h1>Invoice</h1>"]),
]);
file_put_contents("invoice.pdf", curl_exec($ch));

Go

body, _ := json.Marshal(map[string]any{"html": "<h1>Invoice</h1>"})
req, _ := http.NewRequest("POST", "https://pdfgeny.com/api/v1/render", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()

Quotas and errors

Check your remaining balance at any time:

curl https://pdfgeny.com/api/v1/usage -H "Authorization: Bearer $PDFGENY_KEY"
{"plan":"free","quota":50,"used":3,"remaining":47,"watermark":true,"resets":"2026-10-01"}

Errors share one shape: {"error": {"code": "quota_exceeded", "message": "…", "docs": "…"}}. Codes: missing_api_key, invalid_api_key, invalid_input, payload_too_large, quota_exceeded, rate_limited, too_many_concurrent, render_failed, not_found, invalid_signature.

Rate limits

Per key: Free 1 request/s and 1 concurrent render; Starter 5/s and 3; Growth 10/s and 5; Scale 20/s and 10. Over the limit you get 429 with retry_after. Queued jobs are not subject to the concurrency limit — that is what they are for.

StatusMeaning
401Key missing, unknown, revoked, or the account email is not confirmed yet.
400Malformed body, or neither/both of html and url.
413HTML payload above 2 MB.
422The page failed to render — bad markup, or a URL that never loaded.
429Quota reached, rate limit, or too many concurrent renders — see error.code.
403Document link signature invalid or expired.
404Job, batch or document not found (or expired).

Documents on the free plan carry a small footer line. Any paid plan removes it.