# Python SDK

> Render PDFs from Python 3.9+ with the quirepdf package, standard library only, with TypedDicts for each template and a single QuireError.

```bash
pip install quirepdf
```

```python
from quirepdf import Quire

client = Quire()  # reads QUIRE_API_KEY

order = {
    "order_id": "ORD-88213",
    "customer": {"name": "Lena Fischer", "email": "lena@example.de"},
    "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

```python
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

```python
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](/guides/generate-pdf-from-json-python).

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

## Typed template data

```python
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

```python
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

```python
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

```python
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](/docs/errors) for every type.