HTML to PDF in Python

Python's local options all come with baggage: wkhtmltopdf is abandoned and renders 2010-era CSS, WeasyPrint needs system libraries that fight your Docker image, and driving headless Chrome from Python means babysitting browser processes. A rendering API is one requests.post.

1. Get an API key

import requests

res = requests.post(
    "https://inkpdf.dev/v1/keys/free",
    json={"email": "you@company.com"},
)
print(res.json()["apiKey"])  # shown once — save it

Shortcut: the official client

pip install inkpdf-client — stdlib only, no dependencies:

from inkpdf import InkPDF

client = InkPDF(os.environ["INKPDF_KEY"])
open("invoice.pdf", "wb").write(client.invoice(invoice_data))
open("report.pdf", "wb").write(client.markdown("# Q2 Report", theme="serif"))

2. Convert HTML to PDF

import os
import requests

res = requests.post(
    "https://inkpdf.dev/v1/pdf",
    headers={"Authorization": f"Bearer {os.environ['INKPDF_KEY']}"},
    json={
        "html": "<h1>Hello from Python</h1><p>Rendered by real Chromium.</p>",
        "options": {"format": "Letter"},
    },
)
res.raise_for_status()
with open("hello.pdf", "wb") as f:
    f.write(res.content)

3. Generate an invoice from a dict (no HTML)

Post plain data against a built-in template — totals and tax are computed for you:

invoice = {
    "template": "invoice",
    "data": {
        "brand": {"name": "Acme Studio", "color": "#0f766e"},
        "invoiceNumber": "INV-2043",
        "issueDate": "2026-07-01",
        "dueDate": "2026-07-31",
        "currency": "USD",
        "to": {"name": "Northwind Traders", "address": "1 Market St, SF"},
        "items": [
            {"description": "Design work", "quantity": 10, "unitPrice": 95},
        ],
        "taxRate": 8.5,
    },
}
res = requests.post(
    "https://inkpdf.dev/v1/pdf",
    headers={"Authorization": f"Bearer {os.environ['INKPDF_KEY']}"},
    json=invoice,
)

4. Django view: PDF as a response

from django.http import HttpResponse

def invoice_pdf(request, invoice_id):
    data = build_invoice_data(invoice_id)
    res = requests.post(
        "https://inkpdf.dev/v1/pdf",
        headers={"Authorization": f"Bearer {settings.INKPDF_KEY}"},
        json={"template": "invoice", "data": data},
        timeout=60,
    )
    res.raise_for_status()
    response = HttpResponse(res.content, content_type="application/pdf")
    response["Content-Disposition"] = f'attachment; filename="{invoice_id}.pdf"'
    return response

The same pattern works in Flask (send_file(BytesIO(res.content), …)) and FastAPI (Response(res.content, media_type="application/pdf")).

Markdown reports

Generating reports from data pipelines? Send Markdown and pick a theme:

res = requests.post(
    "https://inkpdf.dev/v1/pdf",
    headers={"Authorization": f"Bearer {os.environ['INKPDF_KEY']}"},
    json={"markdown": report_md, "theme": "serif"},
)

Error handling

Errors are JSON with stable codes — validation_error (400), quota_exceeded (429), render_error (408/422/500). Check res.json()["message"] on any non-200; it says exactly what to fix. Only successful renders count against your quota.

Try it without writing code

The playground renders live against the real pipeline — 20 free renders a day, no signup.