# Quickstart

> Create a test API key, render your first PDF from HTML, get a signed download URL and render a stored template by ID.



This guide takes about five minutes. You will create a test key, render a PDF from HTML, get a signed download link, and then render a template stored in your workspace.

The examples use cURL, TypeScript with the built-in `fetch` of Node.js 18 or later, and Python with the `requests` package.

## 1. Create an account and a test key [#1-create-an-account-and-a-test-key]

1. Sign up at [https://app.dynamicdocumentapi.com](https://app.dynamicdocumentapi.com).
2. Open **API keys** and create a key in **test** mode.
3. Copy the key. It is shown only once.

Test keys work before you verify your email address. Test renders are free, carry a "TEST" watermark and are deleted after 24 hours, so you can experiment without using any of your renders.

## 2. Export the key [#2-export-the-key]

Keep the key out of your source code. The examples read it from an environment variable:

```bash
export DYNAMIC_DOCUMENT_API_KEY="dda_test_…"
```

## 3. Render your first PDF [#3-render-your-first-pdf]

This request renders an HTML snippet with one variable and returns the PDF itself as the response body (`delivery.type: "binary"`):

<CodeBlockTabs defaultValue="cURL">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="cURL">
      cURL
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="cURL">
    ```bash
    curl https://api-eu.dynamicdocumentapi.com/v1/renders \
      -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
      -H "Idempotency-Key: $(uuidgen)" \
      -H "Content-Type: application/json" \
      -d '{
        "input": { "type": "html", "html": "<h1>Hello {{ name }}</h1>" },
        "data": { "name": "World" },
        "output": { "format": "pdf", "pdf": { "paper": { "size": "A4" } } },
        "delivery": { "type": "binary" }
      }' \
      -o hello.pdf
    ```
  </CodeBlockTab>

  <CodeBlockTab value="TypeScript">
    ```ts
    import { randomUUID } from "node:crypto";
    import { writeFile } from "node:fs/promises";

    const response = await fetch("https://api-eu.dynamicdocumentapi.com/v1/renders", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.DYNAMIC_DOCUMENT_API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": randomUUID(),
      },
      body: JSON.stringify({
        input: { type: "html", html: "<h1>Hello {{ name }}</h1>" },
        data: { name: "World" },
        output: { format: "pdf", pdf: { paper: { size: "A4" } } },
        delivery: { type: "binary" },
      }),
    });
    if (!response.ok) throw new Error(`Render failed (${response.status}): ${await response.text()}`);

    await writeFile("hello.pdf", Buffer.from(await response.arrayBuffer()));
    console.log(`Saved hello.pdf: ${response.headers.get("X-Pages")} page(s), ${response.headers.get("X-Billed-Renders")} billed render(s)`);
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    import os
    import uuid

    import requests

    response = requests.post(
        "https://api-eu.dynamicdocumentapi.com/v1/renders",
        headers={
            "Authorization": f"Bearer {os.environ['DYNAMIC_DOCUMENT_API_KEY']}",
            "Idempotency-Key": str(uuid.uuid4()),
        },
        json={
            "input": {"type": "html", "html": "<h1>Hello {{ name }}</h1>"},
            "data": {"name": "World"},
            "output": {"format": "pdf", "pdf": {"paper": {"size": "A4"}}},
            "delivery": {"type": "binary"},
        },
        timeout=90,
    )
    response.raise_for_status()

    with open("hello.pdf", "wb") as f:
        f.write(response.content)
    print(f"Saved hello.pdf: {response.headers['X-Pages']} page(s)")
    ```
  </CodeBlockTab>
</CodeBlockTabs>

Save the TypeScript example as `hello.mjs` and run it with `node hello.mjs`. Open `hello.pdf`: it says "Hello World" and carries the test watermark.

What happened:

* `input` describes what to render. The template engine replaced `{{ name }}` with the value from `data`.
* `output` selects the format and [PDF options](/docs/pdf-options) such as the paper size.
* `delivery.type: "binary"` returns the file as the response body. The `X-Render-Id`, `X-Pages` and `X-Billed-Renders` response headers describe the render.
* The `Idempotency-Key` header makes retries safe: repeating the request with the same key returns the original result instead of rendering again.

> **If the request fails**
>
> Errors are returned as JSON problem details with a `code` such as `invalid_api_key` or `validation_error`. With cURL, the error body ends up in `hello.pdf`, so open it in a text editor. See [Errors](/docs/errors) for every code.

## 4. Get a signed download URL [#4-get-a-signed-download-url]

For most applications it's more convenient to store the file and hand out a link. With `delivery.type: "url"` (the default whenever we keep a [hosted copy](/docs/storage#hosted-copy)), the response is a render object whose files have a signed, expiring URL:

<CodeBlockTabs defaultValue="cURL">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="cURL">
      cURL
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="cURL">
    ```bash
    curl https://api-eu.dynamicdocumentapi.com/v1/renders \
      -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
      -H "Idempotency-Key: $(uuidgen)" \
      -H "Content-Type: application/json" \
      -d '{
        "input": { "type": "html", "html": "<h1>Hello {{ name }}</h1>" },
        "data": { "name": "World" },
        "output": { "format": "pdf", "filename": "hello.pdf" },
        "delivery": { "type": "url", "expires_in": 3600 }
      }'
    ```
  </CodeBlockTab>

  <CodeBlockTab value="TypeScript">
    ```ts
    import { randomUUID } from "node:crypto";

    const response = await fetch("https://api-eu.dynamicdocumentapi.com/v1/renders", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.DYNAMIC_DOCUMENT_API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": randomUUID(),
      },
      body: JSON.stringify({
        input: { type: "html", html: "<h1>Hello {{ name }}</h1>" },
        data: { name: "World" },
        output: { format: "pdf", filename: "hello.pdf" },
        delivery: { type: "url", expires_in: 3600 },
      }),
    });
    if (!response.ok) throw new Error(`Render failed (${response.status}): ${await response.text()}`);

    const render = await response.json();
    console.log(render.status, render.files[0].url);
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    import os
    import uuid

    import requests

    response = requests.post(
        "https://api-eu.dynamicdocumentapi.com/v1/renders",
        headers={
            "Authorization": f"Bearer {os.environ['DYNAMIC_DOCUMENT_API_KEY']}",
            "Idempotency-Key": str(uuid.uuid4()),
        },
        json={
            "input": {"type": "html", "html": "<h1>Hello {{ name }}</h1>"},
            "data": {"name": "World"},
            "output": {"format": "pdf", "filename": "hello.pdf"},
            "delivery": {"type": "url", "expires_in": 3600},
        },
        timeout=90,
    )
    response.raise_for_status()

    render = response.json()
    print(render["status"], render["files"][0]["url"])
    ```
  </CodeBlockTab>
</CodeBlockTabs>

The response looks like this (shortened):

```json
{
  "id": "rnd_01J9ZM1X3F7R8K2C4V6B8N0P2Q",
  "object": "render",
  "status": "succeeded",
  "mode": "sync",
  "test": true,
  "region": "eu",
  "input_type": "html",
  "output_format": "pdf",
  "files": [
    {
      "id": "file_01J9ZM4T6W8Y0A2C4E6G8J0K2M",
      "format": "pdf",
      "filename": "hello.pdf",
      "bytes": 18342,
      "pages": 1,
      "url": "https://files-eu.dynamicdocumentapi.com/f/…",
      "url_expires_at": "2026-09-17T16:06:34Z"
    }
  ],
  "billed_renders": 0,
  "created_at": "2026-09-17T15:06:33Z",
  "completed_at": "2026-09-17T15:06:34Z"
}
```

Treat the URL as opaque and don't store it: it stops working at `url_expires_at`. Call `GET /v1/renders/{id}` to get a fresh URL for as long as the file is retained. The [render object](/docs/renders#the-render-object) reference describes every field.

## 5. Render a stored template [#5-render-a-stored-template]

Inline HTML is useful for experiments. In production you usually keep the design in a template, so that designers can change it without a deployment and every change is versioned.

1. In the dashboard, create a template or start from one in the [gallery](/templates).
2. Publish it. Publishing creates version `1` and makes it the `live` version.
3. Copy the template ID (`tpl_…`) from the editor.

Then render it with your data:

<CodeBlockTabs defaultValue="cURL">
  <CodeBlockTabsList>
    <CodeBlockTabsTrigger value="cURL">
      cURL
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="TypeScript">
      TypeScript
    </CodeBlockTabsTrigger>

    <CodeBlockTabsTrigger value="Python">
      Python
    </CodeBlockTabsTrigger>
  </CodeBlockTabsList>

  <CodeBlockTab value="cURL">
    ```bash
    curl https://api-eu.dynamicdocumentapi.com/v1/renders \
      -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
      -H "Idempotency-Key: invoice-2026-0042" \
      -H "Content-Type: application/json" \
      -d '{
        "input": { "type": "template", "template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C" },
        "data": {
          "number": "2026-0042",
          "customer": { "name": "Example GmbH" },
          "items": [ { "description": "Consulting", "quantity": 2, "unit_price": 450 } ]
        },
        "output": { "format": "pdf", "filename": "invoice-{{ data.number }}.pdf" }
      }'
    ```
  </CodeBlockTab>

  <CodeBlockTab value="TypeScript">
    ```ts
    const response = await fetch("https://api-eu.dynamicdocumentapi.com/v1/renders", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.DYNAMIC_DOCUMENT_API_KEY}`,
        "Content-Type": "application/json",
        "Idempotency-Key": "invoice-2026-0042",
      },
      body: JSON.stringify({
        input: { type: "template", template_id: "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C" },
        data: {
          number: "2026-0042",
          customer: { name: "Example GmbH" },
          items: [{ description: "Consulting", quantity: 2, unit_price: 450 }],
        },
        output: { format: "pdf", filename: "invoice-{{ data.number }}.pdf" },
      }),
    });
    if (!response.ok) throw new Error(`Render failed (${response.status}): ${await response.text()}`);

    const render = await response.json();
    console.log(render.files[0].url);
    ```
  </CodeBlockTab>

  <CodeBlockTab value="Python">
    ```python
    import os

    import requests

    response = requests.post(
        "https://api-eu.dynamicdocumentapi.com/v1/renders",
        headers={
            "Authorization": f"Bearer {os.environ['DYNAMIC_DOCUMENT_API_KEY']}",
            "Idempotency-Key": "invoice-2026-0042",
        },
        json={
            "input": {"type": "template", "template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C"},
            "data": {
                "number": "2026-0042",
                "customer": {"name": "Example GmbH"},
                "items": [{"description": "Consulting", "quantity": 2, "unit_price": 450}],
            },
            "output": {"format": "pdf", "filename": "invoice-{{ data.number }}.pdf"},
        },
        timeout=90,
    )
    response.raise_for_status()
    print(response.json()["files"][0]["url"])
    ```
  </CodeBlockTab>
</CodeBlockTabs>

The keys in `data` must match the variables your template uses. `GET /v1/templates/{id}/schema` returns the JSON Schema of the data a template expects.

A few variations:

* Pin a version with `"version": 3` in `input`. Without it, the `live` version is used, so publishing a new version changes future renders.
* Render the unpublished draft with `"version": "draft"` and a test key.
* Use the shorter convenience endpoint `POST /v1/pdf/from-template`, which takes `template_id`, `data` and PDF options at the top level. See [convenience endpoints](/docs/renders#convenience-endpoints).
* Here the idempotency key is derived from the invoice number, so a retried request can never produce a second invoice PDF.

## 6. Go live [#6-go-live]

Before you switch to a live key, work through this list:

1. **Verify your email address.** Live keys can't render until it is verified.
2. **Create a live key with only the scopes you need.** A backend that renders and downloads documents needs `render:write` and `renders:read`. Restrict the key to your servers' IP ranges where possible.
3. **Store the key as a secret.** Use your platform's secret manager or environment variables, and never send the key to browsers or mobile apps.
4. **Choose a plan and review your monthly top-up limit.** See [Plans and limits](/docs/plans-and-limits).
5. **Send an `Idempotency-Key` with every POST request**, and reuse the same key when you retry.
6. **Handle errors deliberately.** Retry `429` and `5xx` responses after `Retry-After`, fix `4xx` requests instead of retrying them, and log the `request_id`. See [Errors](/docs/errors).
7. **Use async mode and webhooks for long renders.** Anything that may exceed your plan's sync timeout should use `mode: "async"`. [Verify webhook signatures](/docs/webhooks#verify-signatures).
8. **Decide how long files live.** Set a workspace retention period, use `delivery.expires_in` for link lifetimes, or use zero-retention delivery for sensitive data.
9. **Subscribe to status updates** at [https://status.dynamicdocumentapi.com](https://status.dynamicdocumentapi.com).
