Skip to main content

castpdf

The official Python client for the CastPDF API: render PDFs from your templates or from HTML, issue EU e-invoices (Factur-X, ZUGFeRD, XRechnung), run batches and manage webhooks.

  • Sync (CastPDF) and asyncio (AsyncCastPDF) clients on httpx.
  • Type hints throughout (py.typed), with TypedDicts for requests and responses.
  • Retries with backoff on rate limits, server errors and network errors; idempotency keys; typed exceptions.
  • A webhook signature helper for the CastPDF-Signature header.

Requires Python 3.10 or newer.

Install

pip install castpdf

Quick start

Create an API key in the CastPDF dashboard (cpdf_test_... keys make watermarked test documents that are not counted).

from castpdf import CastPDF

client = CastPDF()  # reads CASTPDF_API_KEY, or pass CastPDF("cpdf_live_...")

pdf = client.pdfs.create(
    template_id="0b6f0c2e-6d4e-4a43-9a43-3f0f6c1b7a10",
    data={"customer": {"name": "Ada Lovelace"}, "items": [{"name": "Consulting", "qty": 2, "price": 120}]},
    test=True,
)
print(pdf["url"])  # signed download link, valid until pdf["expires_at"]

Async:

import asyncio
from castpdf import AsyncCastPDF

async def main() -> None:
    async with AsyncCastPDF() as client:
        pdf = await client.pdfs.create(html="<h1>Hello</h1>", test=True)
        print(pdf["url"])

asyncio.run(main())

Both clients can be used as context managers (with CastPDF() as client:) to close their connection pool, and take http_client= to use your own httpx.Client / httpx.AsyncClient (proxies, custom transports).

PDFs

Method Request Returns
pdfs.create(**params) response: "url" dict with a signed url (the document is stored)
pdfs.create_file(**params) response: "binary" PdfFile(content, content_type, filename, document_id, pages, warnings)
pdfs.create_async(**params) async: true {"id", "status": "queued"}; the result arrives as a webhook
from pathlib import Path

# From raw HTML, straight to a file
f = client.pdfs.create_file(html="<h1>Hello {{ name }}</h1>", data={"name": "Ada"}, format="A4", margins="20mm")
Path(f.filename or "out.pdf").write_bytes(f.content)

# Queued: poll until done (or listen for the pdf.created webhook)
queued = client.pdfs.create_async(template_id=template_id, data=data, metadata={"order": "1042"})
doc = client.pdfs.wait_for(queued["id"])

# Look a document up again (a fresh signed url while it is stored)
again = client.pdfs.get(doc["id"])

test=True makes a watermarked test document that is not counted, even with a live key. A test key always makes test documents. The other options (mode, format, orientation, margins, deliver, metadata, merge_data, ...) are described in the API reference.

E-invoices

Pass e_invoice to any PDF method. Factur-X and ZUGFeRD return a PDF/A-3b with the XML embedded; XRechnung returns the XML itself (content_type == "application/xml").

invoice = client.pdfs.create_file(
    e_invoice={
        "standard": "zugferd",
        "profile": "en16931",
        "invoice": {
            "number": "RE-2026-0042",
            "issue_date": "2026-10-10",
            "currency": "EUR",
            "seller": {
                "name": "Muster GmbH",
                "vat_id": "DE123456789",
                "address": {"line1": "Hauptstr. 1", "city": "Berlin", "postal_code": "10115", "country_code": "DE"},
            },
            "buyer": {"name": "Kunde AG", "address": {"city": "Hamburg", "postal_code": "20095", "country_code": "DE"}},
            "payment": {"iban": "DE02120300000000202051", "terms": "Net 14 days"},
            "lines": [{"name": "Consulting", "quantity": 8, "unit_code": "HUR", "unit_price": "95.00", "vat": {"category": "S", "rate": 19}}],
        },
    },
    test=True,
)
print(invoice.e_invoice)  # "zugferd/en16931"

Documents

page = client.documents.list(limit=50)        # {"data": [...], "next_before": ...}
for doc in client.documents.iterate():         # every document, newest first
    print(doc["id"], doc["status"])

Templates

templates = client.templates.list()["data"]
fields = client.templates.fields(templates[0]["id"])   # what to send as `data`
template = client.templates.get(template_id, version=3)
client.templates.create(name="Invoice", html="<h1>{{ number }}</h1>", sample_data={"number": "RE-1"})  # live keys
client.templates.update(template_id, html="<h1>Invoice {{ number }}</h1>")  # a new version
client.templates.pin(template_id, 2)                  # None unpins
preview = client.templates.preview(template_id, data={"number": "RE-1"})  # watermarked, not counted
client.templates.versions(template_id)
client.templates.delete(template_id)

Batches

batch = client.batches.create(
    template_id=template_id,
    items=[{"data": c, "filename": f"{c['id']}.pdf"} for c in customers],
    idempotency_key="march-statements",
)
done = client.batches.get(batch["id"])          # done["zip"]["url"] when status is "completed"
for item in client.batches.iterate_items(batch["id"]):
    print(item["index"], item["status"])
client.batches.cancel(batch["id"])

Webhooks

endpoint = client.webhooks.create(url="https://example.com/castpdf", events=["pdf.created", "pdf.failed"])
print(endpoint["secret"])  # shown only now (and after rotate_secret): store it

client.webhooks.update(endpoint["id"], enabled=False)
client.webhooks.send_test(endpoint["id"])
client.webhooks.deliveries(endpoint["id"])
client.webhooks.rotate_secret(endpoint["id"])  # the old secret keeps signing for 24 hours

Verifying signatures

CastPDF signs every delivery with CastPDF-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">. Verify against the raw body, before parsing it:

import os
from flask import Flask, abort, request
from castpdf import SignatureVerificationError, construct_event

app = Flask(__name__)

@app.post("/castpdf")
def castpdf_webhook():
    try:
        event = construct_event(
            request.get_data(),                       # raw bytes
            request.headers.get("CastPDF-Signature"),
            os.environ["CASTPDF_WEBHOOK_SECRET"],
        )
    except SignatureVerificationError:
        abort(400)
    if event["type"] == "pdf.created":
        print("ready:", event["data"]["url"])
    return "", 200

verify_signature(payload, header, secret) returns True or False instead of raising. Signatures older than 5 minutes are refused (tolerance_seconds= changes that). During the 24 hours after a secret rotation the header carries two v1 values, and either secret verifies.

Account

me = client.me()  # team, plan and key mode: a cheap connection test
client.account.usage()

Errors

Every exception extends CastPDFError. Answers with an error status raise an APIError subclass with status, code (for example "template_not_found"), message, docs_url, details and request_id:

Class Status
BadRequestError 400, 422
AuthenticationError 401
PaymentRequiredError 402
PermissionDeniedError 403
NotFoundError 404
ConflictError 409
GoneError 410
PayloadTooLargeError 413
RateLimitError 429 (retry_after in seconds)
InternalServerError 5xx
APIConnectionError, APITimeoutError no answer
from castpdf import NotFoundError, RateLimitError

try:
    client.pdfs.create(template_id=template_id, data=data)
except NotFoundError:
    print("No such template")
except RateLimitError as err:
    print(f"Retry in {err.retry_after} s")

Retries, idempotency and timeouts

  • A 429 rate_limited is retried (it was never processed), waiting for retry-after. Daily limits (storage_limit_reached, email_limit_reached) are not.
  • 5xx answers, timeouts and network errors are retried for GET, PUT and DELETE, and for POST only when the request carries an Idempotency-Key, so a PDF is never rendered twice by a retry.
  • Waits grow exponentially with jitter (0.5 s, 1 s, 2 s, ... up to 8 s). max_retries defaults to 2.
client = CastPDF(
    max_retries=3,
    timeout=60.0,                 # seconds per attempt; default 120
    auto_idempotency_keys=True,   # generate a key for pdfs.create* and batches.create when none is given
)

client.pdfs.create(template_id=template_id, data=data, idempotency_key=f"invoice-{order_id}")
client.me(request_options={"timeout": 10, "max_retries": 0, "headers": {"x-trace": "abc"}})

The same key and body within 24 hours return the first result instead of rendering again. A request with a key is always stored by the API (to replay it), also for create_file.

Version history

  • 0.1.0: first release.

License

MIT

Metadata

Release files for castpdf 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for castpdf 0.1.0
File Size Uploaded
castpdf-0.1.0.tar.gz 23.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for castpdf 0.1.0
File Interpreter ABI Platform
castpdf-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 44.9 kB

Release files / castpdf-0.1.0.tar.gz

Download URL castpdf-0.1.0.tar.gz
Size 23.8 kB
Tags Source
SHA-256 checksum
How to use checksums
37f973e7579dcc1ad7a7a04bc6a153b4949e160a89595fd9d4967a1aa3e75c91
BLAKE2b-256 checksum
How to use checksums
d672ef2c4814397ae4a6df4336e4f0cb2ecfe4fa3a4ea051c64a3caa023df4ca
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release files / castpdf-0.1.0-py3-none-any.whl

Download URL castpdf-0.1.0-py3-none-any.whl
Size 21.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6c32e810791adfc01faffe6bd082cf87c1fb4d9b8aa331b7849e5c358a2e8e7b
BLAKE2b-256 checksum
How to use checksums
73265e0c5737fd58f7bc3768e3ea8e338ea9696a84fb3b1deb87fbf44a8f0fdb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page