PDF Template API

PDF template API: design once, generate thousands

Write a template in HTML and CSS, store it once, then send JSON. Every PDF comes out on-brand, with loops, conditions, currencies and QR codes filled in by the engine.

  • 50 free renders/month
  • No credit card
  • Jinja syntax

What is a PDF template API?

A PDF template API stores a document layout with placeholders and fills it with data on request. You design the template once, then send JSON such as a customer name, line items or dates; the API merges the data into the layout, renders it and returns a finished PDF. The layout lives in one place instead of in your code.

PDF template API key facts
EndpointPOST https://api.dynamicdocumentapi.com/v1/pdf/from-template
Template languageJinja syntax (MiniJinja engine): variables, loops, conditions, macros, includes, automatic HTML escaping
Template typesHTML/CSS code templates and Markdown templates
FormattingCurrencies, numbers, percentages and dates in any locale, based on Unicode CLDR data (ICU4X)
CodesQR codes and 16 barcode types, including Code 128, EAN-13, UPC-A, ITF-14 and GS1-128
VersioningDraft, published versions, render any version by number, one-call rollback
OutputPDF, or PNG/JPEG/WebP from the same template
PriceFree: 50 renders/month. Paid from €15/month (billed annually) for 3,000 renders, auto top-ups from €6 per 1,000

From template to PDF in three steps

  1. Write the template

    HTML and CSS with Jinja placeholders. Start from your existing layout; you can write templates in plain HTML and CSS with the same print features as a one-off render.

  2. Publish a version

    Test the draft with sample data and a test key, then publish. The template gets a stable ID and a version number.

  3. Send JSON, get PDFs

    Each request carries the template ID and your data. The API returns the PDF or a signed link to it.

Generate a PDF from JSON

The request names the template and passes your data. The JSON can have any shape: nested objects and arrays reach the template exactly as you send them, so you rarely need to reshape data from your database or billing system.

curl https://api.dynamicdocumentapi.com/v1/pdf/from-template \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C",
    "version": "live",
    "data": {
      "number": "2026-0042",
      "customer": { "name": "Acme Co." },
      "items": [
        { "name": "API renders", "total": 1200.00 },
        { "name": "Support",     "total": 99.00 }
      ],
      "paid": false,
      "issued": "2026-09-22"
    },
    "filename": "invoice-{{ data.number }}.pdf"
  }'
// Node.js 18+ (built-in fetch)
const res = await fetch("https://api.dynamicdocumentapi.com/v1/pdf/from-template", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.DYNAMIC_DOCUMENT_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    template_id: "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C",
    data: {
      number: "2026-0042",
      customer: { name: "Acme Co." },
      items: [{ name: "API renders", total: 1200 }, { name: "Support", total: 99 }],
      paid: false,
      issued: "2026-09-22",
    },
  }),
});

const render = await res.json();
console.log(render.files[0].url);
import os
import requests

res = requests.post(
    "https://api.dynamicdocumentapi.com/v1/pdf/from-template",
    headers={"Authorization": f"Bearer {os.environ['DYNAMIC_DOCUMENT_API_KEY']}"},
    json={
        "template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C",
        "data": {
            "number": "2026-0042",
            "customer": {"name": "Acme Co."},
            "items": [{"name": "API renders", "total": 1200},
                      {"name": "Support", "total": 99}],
            "paid": False,
            "issued": "2026-09-22",
        },
        "delivery": "binary",
    },
    timeout=60,
)
res.raise_for_status()
open("invoice.pdf", "wb").write(res.content)
<?php
$payload = [
    'template_id' => 'tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C',
    'data' => [
        'number'   => '2026-0042',
        'customer' => ['name' => 'Acme Co.'],
        'items'    => [
            ['name' => 'API renders', 'total' => 1200],
            ['name' => 'Support', 'total' => 99],
        ],
        'paid'   => false,
        'issued' => '2026-09-22',
    ],
];

$ch = curl_init('https://api.dynamicdocumentapi.com/v1/pdf/from-template');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . getenv('DYNAMIC_DOCUMENT_API_KEY'),
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS     => json_encode($payload),
]);
$render = json_decode(curl_exec($ch), true);
echo $render['files'][0]['url'];

version is optional and defaults to "live", the latest published version. Pin a number such as 7 when a document must always look the same, for example for a contract that was signed in that layout. The filename is a template too.

Template syntax

Templates use Jinja syntax. If you have written a Django, Flask, Jekyll or Shopify Liquid template, you already know most of it. Below are the parts documents need most, each with the template on top and what the PDF shows below.

Variables

Any value from your JSON, including nested fields. Text is HTML-escaped automatically, so a customer called <b>Acme</b> cannot change your layout.

Hello {{ customer.name }},
→ Hello Acme Co.,

Loops for line items and tables

for repeats a block for each item. loop.index, loop.first and loop.last help with numbering and separators, and sum_by adds up a column.

{% for item in items %}
<tr><td>{{ loop.index }}</td><td>{{ item.name }}</td></tr>
{% endfor %}
Total: {{ items | sum_by("total") }}

Conditions

Show a block only when the data says so: a "Paid" stamp, a discount line, a second-language footer, or a different address block for business customers.

{% if paid %}<p class="stamp">Paid</p>
{% elif due_days > 30 %}<p>Overdue</p>{% endif %}

Currencies, numbers and dates in any locale

Formatting uses Unicode CLDR data, the same data behind operating systems and browsers. Symbols, separators, symbol position and month names are correct for every locale, without formatting code in your app.

{{ 1234.5 | format_currency("EUR", "de") }}  → 1.234,50 €
{{ 1234.5 | format_currency("USD", "en-US") }}  → $1,234.50
{{ issued | format_date("long", "en-US") }}  → September 22, 2026
{{ 0.256 | format_percent }}  → 26%

number_to_words spells amounts out in English, German, French, Spanish or Dutch, for checks and payment slips.

Images, QR codes and barcodes

qrcode draws a sharp vector QR code with the error correction level you choose. barcode supports 16 types, including Code 128, Code 39, EAN-13, EAN-8, UPC-A, ITF-14, ISBN and GS1-128, and computes check digits. Images load from any public URL.

{{ ticket.url | qrcode(size="30mm", ecc="Q") }}
{{ product.gtin | barcode(type="ean13") }}

Markdown inside templates

Free-text fields from a CMS or a form often contain Markdown. The markdown filter turns them into clean, sanitized HTML inside your layout: headings, lists, tables, links and footnotes.

<section class="notes">{{ notes | markdown }}</section>

Also available: macro for reusable blocks such as an address, include for shared partials like a legal footer, page_break() to start a new page, and table to turn a list of objects into an HTML table in one line.

Template versioning and safe changes

Changing a live template is where document automation usually breaks: a designer moves a field, and invoices go out half-empty. Templates here work like releases.

  • Draft and live. Edits go into a draft. Production keeps rendering the live version until the draft is published.
  • Every version stays. Each publish creates a numbered version with an optional note. Render any of them with "version": 7.
  • One-call rollback. POST /v1/templates/{id}/rollback makes an earlier version live again.
  • Test before you ship. Render the draft with a test key: test renders cost nothing, carry a "TEST" watermark and are deleted after a day.
  • Catch bad data early. Each template can declare a data schema. With strict validation on, a request with missing or mistyped fields fails with a list of what is wrong, instead of producing a broken PDF.

The same template can serve more than one output: render it as a PDF for print and as PNG, JPEG or WebP for email or social sharing. Templates are one of four inputs of our PDF generation API, next to raw HTML, URLs and Markdown.

Thousands of documents from one template

For month-end invoices, a cohort of certificates or one report per client, send a batch instead of thousands of single requests. POST /v1/batches takes a template ID and a list of items, or a CSV file with a column mapping. Each item becomes one PDF, and you can also have all of them merged into a single PDF.

Progress arrives by webhook (batch.progress, batch.completed), and retry-failed re-runs only the items that failed. Batches are available on paid plans.

Where teams use PDF templates

  • Invoices and receipts. Line items, taxes you calculated, totals in the customer's currency. See how to build an invoice template that breaks cleanly across pages.
  • Reports. One layout, one PDF per client, with charts drawn by JavaScript before capture.
  • Certificates and tickets. Names, dates and a QR code for verification or check-in.
  • Contracts and offers. Clauses switched on and off with conditions, pinned to the version the customer signed.

PDF template API pricing

Free

€0

50 renders per month, full REST API. Enough to build and test a template.

Starter

€15 / month

3,000 renders per month, billed annually (€19 billed monthly). About €0.005 per render.

Template limits and batch sizes grow with each plan. See template limits per plan.

FAQ

PDF template API: frequently asked questions

Which template language does the API use?

Jinja syntax, the template language of Python's Jinja2 and of many static-site generators. You get variables, for loops, if conditions, macros, includes and filters, plus built-in filters for currency, dates, QR codes and barcodes. HTML output is escaped automatically, so data from your users cannot break the layout.

Can I import my existing HTML templates?

Yes. A template is HTML and CSS, so an existing invoice or report layout works as it is. Replace the hard-coded values with {{ variables }} step by step; parts you have not converted yet still render.

Can non-developers edit a template without breaking the API integration?

Edits happen in a draft. Your application keeps rendering the live version until someone publishes the draft, and every published version stays available, so a mistake can be rolled back with one call. With strict data validation turned on, a render whose JSON does not match the template's schema fails with a clear error instead of producing a wrong document.

Can I generate a PDF directly from JSON?

Yes. Send your data as JSON in data together with a template_id to POST /v1/pdf/from-template. The JSON can have any shape: nested objects and arrays are available in the template exactly as you send them.

How do I handle tables that run over several pages?

Write the table in plain HTML with a <thead>. The engine repeats the header row on every page, and tr { break-inside: avoid; } keeps rows from being split. Use the page_break() function to force a new page before a section.

Can the same template produce an image?

Yes. POST /v1/images/from-template renders the same template as PNG, JPEG or WebP, for example a certificate as a PDF for printing and as a PNG for sharing.

Build your first template free

50 renders a month on the free plan, no credit card, auto top-ups on paid plans.

Start free