Generate PDF invoices from Python in 20 lines
A complete, working example: invoice data as a dict, a template, one API call, a PDF on disk. Then the same thing for a batch of a hundred.
Updated 31 August 2026
You have invoice data in your database and need PDFs. The options are a PDF library (ReportLab, fpdf2 — you draw every line), an HTML renderer you host (WeasyPrint, Playwright — you run it), or an API. This is the API version, because it is the shortest.
One invoice
import requests
KEY = "pg_…"
invoice = {
"company": "Northwind Studio Ltd",
"client": "Acme Corporation",
"number": "INV-2026-014",
"issued": "31 August 2026",
"due": "30 September 2026",
"item1": "Design retainer, August", "qty1": "1", "price1": "2400.00",
"item2": "Illustration set", "qty2": "6", "price2": "120.00",
"subtotal": "3120.00", "tax_label": "VAT 20%", "tax": "624.00", "total": "3744.00",
"currency": "£",
}
r = requests.post(
"https://pdfgeny.com/api/v1/render",
headers={"Authorization": f"Bearer {KEY}"},
json={"template": "invoice", "data": invoice, "filename": "INV-2026-014.pdf"},
timeout=60,
)
r.raise_for_status()
open("INV-2026-014.pdf", "wb").write(r.content)
Line amounts are computed for you from quantity and price. Fields you leave out take the template's example values, so start with two fields and grow.
Your own layout
If the built-in invoice does not match your brand, render your own HTML instead — Jinja2 or Django templates on your side, `"html": rendered` in the request. Everything else stays the same.
A hundred invoices
Month-end runs are where synchronous loops hurt: 100 requests in sequence take a couple of minutes and hold a connection open the whole time. Use the batch endpoint and let it run:
docs = [{"template": "invoice", "data": inv, "filename": f"{inv['number']}.pdf"} for inv in invoices]
r = requests.post("https://pdfgeny.com/api/v1/batch", headers={"Authorization": f"Bearer {KEY}"},
json={"documents": docs}, timeout=30)
batch_id = r.json()["batch_id"]
# later, or from a webhook:
status = requests.get(f"https://pdfgeny.com/api/v1/batch/{batch_id}", headers={"Authorization": f"Bearer {KEY}"}).json()
for job in status["jobs"]:
if job["status"] == "done":
pdf = requests.get(job["document_url"]).content
Each `document_url` is signed and valid for seven days — long enough to email or archive, short enough not to be a liability.
Errors you will see
- `401 invalid_api_key` — the key is revoked or the account email is not confirmed yet.
- `429 quota_exceeded` — free plan is 50 documents a month; the response says how many you used.
- `422 render_failed` — your HTML did something Chromium refused; the message says what.
Try it: convert HTML to PDF in the browser, or get a free API key — 50 documents a month.
More guides
Writing HTML templates for PDF: a style guide (Jinja, Django, Handlebars)
Templates that render well as PDFs follow a few rules: fixed page geometry, no external state, defensive filters, print CSS. Examples in Jinja and Django syntax.
Issuing 5,000 course certificates in one afternoon
A spreadsheet of names, one landscape template, batch requests of 100, and signed links per student. The full workflow including naming, verification codes and delivery.
Generating receipts at checkout without slowing the checkout
The receipt PDF should never sit between the customer and the order confirmation. A pattern: confirm first, render asynchronously, attach when ready.
URL-to-PDF and SSRF: how a PDF renderer becomes an attack surface
A renderer that fetches URLs can be pointed at your internal network. The attacks, the defences, and what to demand from a PDF API vendor.