Skip to main content

Dynamic Document API for Python

The official SDK for Dynamic Document API: turn templates and JSON into PDFs, images and e-invoices, from EU data centres.

pip install dynamic-document-api

It runs on Python 3.9 and later and depends only on httpx and typing_extensions. There is a synchronous client, DynamicDocumentApi, and an asynchronous one, AsyncDynamicDocumentApi, with the same methods. Keep your API key on the server.

Quickstart

from dynamic_document_api import DynamicDocumentApi

dda = DynamicDocumentApi()  # reads DYNAMIC_DOCUMENT_API_KEY

render = dda.pdf.from_template(
    template_id="tpl_…",
    data={"number": "2026-0042", "customer": {"name": "Ada Lovelace"}},
)

print(render["files"][0]["url"])  # a signed download link

Create a key in the dashboard under API keys. A test key (dda_test_…) renders for free, with a watermark; a live key (dda_live_…) counts against your plan.

With asyncio (or trio), use the async client and await the same calls:

import asyncio

from dynamic_document_api import AsyncDynamicDocumentApi


async def main() -> None:
    async with AsyncDynamicDocumentApi() as dda:
        render = await dda.pdf.from_template(template_id="tpl_…", data={"number": 42})
        print(render["id"])


asyncio.run(main())

Both clients hold a connection pool: use them as a context manager, or call dda.close() (await dda.aclose()) when you are done. Create one client and reuse it.

Renders

Keyword arguments carry the API's own names, exactly as in the API reference, and responses are the API's JSON as plain dicts.

# From HTML, a URL or Markdown
from_html = dda.pdf.from_html(html="<h1>Hello</h1>", pdf={"paper": {"size": "A4"}})
screenshot = dda.images.from_url(url="https://example.com")

# The unified endpoint takes every input type and output format
render = dda.renders.create(
    input={"type": "template", "template_id": "tpl_…"},
    data={"number": 42},
    output={"format": "pdf"},
    mode="async",  # answer at once and render in the background
)
done = dda.wait_for_render(render["id"])  # polls until it is finished

# The file in the response body instead of a link
binary = dda.renders.create_binary(input={"type": "html", "html": "<h1>Hi</h1>"})
pdf_bytes = binary.content

# Download a hosted file, or stream it
pdf = dda.files.download(done["files"][0])
with dda.files.stream(done["files"][0]) as response, open("invoice.pdf", "wb") as out:
    for chunk in response.iter_bytes():
        out.write(chunk)

Previews (dda.previews.pdf(), .image(), .html()) render without storing or billing, for editors.

Batches

batch = dda.batches.create(
    template_id="tpl_…",
    items=[{"data": invoice, "reference": invoice["number"]} for invoice in invoices],
    combine={"zip": True},
)
finished = dda.wait_for_batch(batch["id"])
for item in dda.batches.list_items(batch["id"], status="failed"):
    print(item["index"], item.get("error"))

PDF tools and e-invoices

merged = dda.pdf_tools.merge(sources=[{"render_id": a["id"]}, {"render_id": b["id"]}])
archived = dda.pdf_tools.pdfa(source={"render_id": render["id"]}, level="2b")

# Standalone XRechnung or Factur-X XML
xml = dda.einvoices.create(profile="XRECHNUNG", invoice={...})  # EN 16931 fields

Files for the PDF tools, attachments and batch CSVs go in with dda.uploads.create(Path("items.csv")) (or bytes and a filename); uploads are kept for 24 hours. Invoice amounts can be Decimals: they are sent as decimal strings.

Templates

for template in dda.templates.list():
    print(template["id"], template["name"])

created = dda.templates.create(name="Invoice", type="pdf_code", html="<h1>Invoice {{ number }}</h1>")
dda.templates.update_draft(created["id"], html="<h1>Invoice {{ number }}</h1><p>{{ customer.name }}</p>")
dda.templates.publish(created["id"], note="First version")

Email delivery and storage

Send rendered documents through your own email provider, or upload them to your own bucket or SFTP server.

connection = dda.email_connections.create(
    name="Billing",
    provider="brevo",
    default_from={"email": "billing@example.com", "name": "Example Ltd"},
    credentials={"api_key": os.environ["BREVO_API_KEY"]},
)
dda.email_rules.create(
    "tpl_…",
    name="Send the invoice",
    connection_id=connection["id"],
    to="{{ customer.email }}",
    subject="Invoice {{ number }}",
)

for send in dda.email_sends.list(status="failed"):
    print(send["id"])

dda.storage_destinations.create(
    name="Archive",
    provider="s3",
    config={"bucket": "acme-invoices", "region": "eu-central-1"},
    credentials={"access_key_id": "…", "secret_access_key": "…"},
)

A signed link renders an image or PDF from a template on request, for <img> tags, emails and social cards. sign() builds the URL locally with the link's secret, without an API call:

link = dda.signed_links.create(
    template_id="tpl_…",
    name="Social card",
    formats=["png"],
    allowed_params=["title.text"],
)
# link["secret"] is shown once; store it

url = dda.signed_links.sign(
    link["id"],
    os.environ["DDA_LINK_SECRET"],
    params={"title.text": "Summer Sale"},
    expires_in=3600,
)

Webhooks

dda.webhook_endpoints.create(url="https://example.com/hooks/dda", events=["render.succeeded", "batch.completed"])

# In your handler (Flask here): verify with the raw body, before parsing it
event = dda.webhooks.verify(request.get_data(), request.headers, os.environ["DDA_WEBHOOK_SECRET"])
if event["type"] == "render.succeeded":
    print(event["data"]["object"]["id"])

Pass a list of secrets to verify() while you roll one with dda.webhook_endpoints.roll_secret(). Verification needs no API key: from dynamic_document_api import Webhooks and call Webhooks().verify(...). It accepts the headers of Django, Flask, Starlette/FastAPI and httpx, or a plain dict, and the body as bytes or str.

Configuration

dda = DynamicDocumentApi(
    api_key="dda_live_…",          # default: DYNAMIC_DOCUMENT_API_KEY
    region="eu",                   # the EU (Germany) is the only region today
    timeout=130,                   # seconds per request; above the longest synchronous wait (120 s)
    retry={"max_attempts": 3},     # or False
    auto_idempotency_key=True,     # every POST gets an Idempotency-Key
    telemetry=True,                # the SDK's name and version in User-Agent
    http_client=httpx.Client(),    # default: a client of its own
)

# Per call: your own idempotency key, request id, timeout, retries or headers
dda.pdf.from_html(html=html, idempotency_key=f"invoice-{invoice_id}", timeout=120)

Retries cover network errors, 429 and 5xx, with exponential backoff and jitter, and respect Retry-After. A retried POST reuses its idempotency key, so a render is never billed twice; a timed-out request is not retried. Values are sent exactly as written: data, metadata, HTTP headers, signed-link params, and the config and credentials of connections and destinations keep their keys. None is sent as JSON null; Decimal, date and datetime values become strings. The one renamed argument is usage.retrieve(from_=…), since from is a Python keyword.

Lists

Every list fetches pages as you iterate:

for render in dda.renders.list(status="failed"):
    print(render["id"])

page = dda.renders.list(limit=20).page()
recent = dda.renders.list().all(200)  # pass a limit: a workspace can hold millions

With the async client, iterate with async for and await .page() and .all(). Bracket filters are written as dicts: dda.renders.list(created={"gte": "2026-09-01T00:00:00Z"}, metadata={"customer": "c_981"}).

Errors

Every failure is a DynamicDocumentApiError with status, code, message and request_id, and a subclass per kind of failure:

from dynamic_document_api import RateLimitError, RenderFailedError

try:
    dda.pdf.from_template(template_id=template_id, data=data)
except RenderFailedError as error:
    print(error.code, error.message)  # e.g. template_runtime_error
except RateLimitError:
    print("slow down")

Every error code is listed at https://dynamicdocumentapi.com/docs/errors.

Types

Responses are plain dicts, described by TypedDicts generated from the API's OpenAPI contract, so editors complete the keys and type checkers catch typos. The keyword arguments of each method are typed the same way.

from dynamic_document_api.types import PdfFromTemplateParams, Render

Anything else

dda.request() calls any endpoint with the same authentication, retries and error handling:

account = dda.request("GET", "/account")

The full API is described at https://dynamicdocumentapi.com/docs/api-reference.

Versioning

The SDK follows semantic versioning. While it is 0.x, a minor version can change the API; the changelog is CHANGELOG.md in the source distribution.

Licence

MIT. See LICENSE.

Metadata

Release files for dynamic-document-api 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 dynamic-document-api 0.1.0
File Size Uploaded
dynamic_document_api-0.1.0.tar.gz 128.7 kB Details

Built distribution (wheel)

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

Total release size: 233.6 kB

Release files / dynamic_document_api-0.1.0.tar.gz

Download URL dynamic_document_api-0.1.0.tar.gz
Size 128.7 kB
Tags Source
SHA-256 checksum
How to use checksums
3c7bb5004e540cdc74b0ba35758319cd77e70fa5e74152ea8c651d89067e678f
BLAKE2b-256 checksum
How to use checksums
22bc9a5929021756e84702ad13b6f1ded7ec829d94d9d435a58e5b06e4391843
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.22 {"installer":{"name":"uv","version":"0.9.22","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

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

Download URL dynamic_document_api-0.1.0-py3-none-any.whl
Size 104.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bcc1e79f2d0c0c3c883249da949dfc9dd2f30e4d73809b8e5cec19daaf9a5366
BLAKE2b-256 checksum
How to use checksums
c210e17da81eee338000449bacbf03582ff47663122aa6522c076b203b2ca91f
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.9.22 {"installer":{"name":"uv","version":"0.9.22","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

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