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.