Python SDK

pip install quirepdf
from quirepdf import Quire

client = Quire()  # reads QUIRE_API_KEY

order = {
    "order_id": "ORD-88213",
    "customer": {"name": "Lena Fischer", "email": "[email protected]"},
    "lines": [{"sku": "TS-BLK-M", "name": "Organic tee", "qty": 2, "price": 29.0}],
    "currency": "€",
}
result = client.render(order)
result.save("order.pdf")
print(result.template, result.template_source, result.pages)  # document fallback 1

The package needs Python 3.9 or later and uses only the standard library. The client is synchronous.

Configuration

client = Quire(api_key="qk_live_...", base_url="https://api.quirepdf.dev", timeout=10)
Argument Environment variable Default
api_key QUIRE_API_KEY None; no Authorization header is sent
base_url QUIRE_API_URL https://api.quirepdf.dev
timeout 30.0 seconds per request

The client never follows redirects, so your key is never forwarded to another host.

Render

result = client.render(data, template="invoice", format="pdf")

render(data, template=None, format="pdf", page=None, test=False) returns a RenderResult. With test=True (from 0.2.0) the render is free and watermarked, and isn’t counted.

Field Type Meaning
content bytes The PDF or PNG file
content_type str application/pdf or image/png
pages int Pages in the whole document
template str The template used
template_source str explicit, detected or fallback
hint str or None A template that almost matched, e.g. invoice (seller is required)
render_ms float Server render time
quota Quota or None Quota(limit=…, remaining=…) after this render (None for test renders)
test bool True for watermarked test renders

content, not bytes. The file is in result.content. The TypeScript SDK calls this field bytes, but in Python that name would shadow the built-in type. It matches the content attribute of Python HTTP libraries.

save(path) writes content to a file and returns a pathlib.Path. To send the file over HTTP instead, use content directly; see the FastAPI and Django guide.

Decimal values in your data are serialised as numbers, so you can pass rows from a database without converting prices first.

Typed template data

from quirepdf import Quire
from quirepdf.types import InvoiceData

invoice: InvoiceData = {
    "number": "INV-2026-0142",
    "issued": "1 Oct 2026",
    "due": "15 Oct 2026",
    "seller": {"name": "Northwind Studio", "address": ["221 Market Street", "San Francisco, CA 94105"]},
    "customer": {"name": "Ravi Kumar", "address": ["14 Residency Road", "Bengaluru 560025"]},
    "items": [{"description": "Pro plan", "qty": 1, "unit_price": 49}],
    "status": "due",  # Literal["draft", "due", "paid", "overdue", "void"]
}

client = Quire()
pdf = client.render(invoice, template="invoice")
png = client.render(invoice, template="invoice", format="png", page=1)  # preview one page

quirepdf.types has a TypedDict per template, generated from the JSON Schemas: InvoiceData, ReceiptData, QuoteData, CreditNoteData, StatementData, CertificateData and DocumentData. A type checker such as mypy or Pyright flags missing required keys and invalid values. At runtime they are plain dicts.

Validate for free

check = client.validate({"number": "INV-1"}, template="invoice")
print(check.valid)       # False
print(check.errors[:2])  # [{'path': 'issued', 'message': 'issued is required'}, {'path': 'due', ...}]

validate(data, template=None) returns a ValidationResult with valid, template, source, hint and errors. Nothing is rendered and it doesn’t count against your quota. Invalid data is a result, not an exception.

Templates, usage and keys

client.templates.list()           # [{"name": "certificate", "title": "Certificate", ...}, ...]
client.templates.get("receipt")   # {"template": {...}, "schema": {...}, "sample": {...}}

client.usage()  # {"email": "...", "plan": "free", "period": "2026-10", "used": 3, "limit": 100, "remaining": 97}

new = client.keys.create(name="ci")   # new["api_key"] is shown only now
client.keys.list()                    # [{"id", "prefix", "name", "created_at", "last_used_at", "current"}, ...]
client.keys.revoke(new["key"]["id"])

Timestamps in keys.list() are Unix seconds. current is True for the key the client is using.

Errors

from quirepdf import Quire, QuireError

client = Quire()
try:
    client.render({"number": "INV-1"}, template="invoice")
except QuireError as err:
    print(err.status, err.type)  # 422 invalid_data
    print(err)                   # data has 5 problems
                                 #   - issued is required
                                 #   - due is required ...
    for f in err.fields:
        print(f["path"], f["message"])

Every failure raises QuireError with:

Attribute Meaning
status HTTP status, or 0 when no response arrived
type The API error type (invalid_data, unknown_template, unauthorized, quota_exceeded, …), or network / timeout
message The API’s message
fields A list of {"path", "message"} dicts, for invalid_data

str(err) includes every field problem, so logging the exception is enough. The SDK never retries. See Errors for every type.

View this page as Markdown