Metadata-Version: 2.5
Name: dynamic-document-api
Version: 0.1.0
Summary: Official Dynamic Document API SDK: turn templates and JSON into PDFs, images and e-invoices.
Project-URL: Homepage, https://dynamicdocumentapi.com/docs/sdks-and-integrations
Project-URL: Documentation, https://dynamicdocumentapi.com/docs/api-reference
Project-URL: Changelog, https://dynamicdocumentapi.com/docs/sdks-and-integrations
Author: N.M.M. Noble Minds Media Ltd
License-Expression: MIT
License-File: LICENSE
Keywords: dynamic-document-api,e-invoice,factur-x,html-to-pdf,image-generation,pdf,pdf-generation,screenshot,templates,xrechnung,zugferd
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Office/Business
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx<1,>=0.25
Requires-Dist: typing-extensions>=4.7
Description-Content-Type: text/markdown

# Dynamic Document API for Python

The official SDK for [Dynamic Document API](https://dynamicdocumentapi.com): turn templates and JSON into PDFs,
images and e-invoices, from EU data centres.

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

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

```python
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](https://dynamicdocumentapi.com/docs/api-reference),
and responses are the API's JSON as plain dicts.

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

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

```python
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 `Decimal`s: they are sent as decimal
strings.

## Templates

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

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

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

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

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

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

```python
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 `TypedDict`s 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.

```python
from dynamic_document_api.types import PdfFromTemplateParams, Render
```

## Anything else

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

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