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": "…"},
)
Signed links
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)
| File | Size | Uploaded | |
|---|---|---|---|
| dynamic_document_api-0.1.0.tar.gz | 128.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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}
|