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.,
PDF Template API
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.
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.
| Endpoint | POST https://api.dynamicdocumentapi.com/v1/pdf/from-template |
|---|---|
| Template language | Jinja syntax (MiniJinja engine): variables, loops, conditions, macros, includes, automatic HTML escaping |
| Template types | HTML/CSS code templates and Markdown templates |
| Formatting | Currencies, numbers, percentages and dates in any locale, based on Unicode CLDR data (ICU4X) |
| Codes | QR codes and 16 barcode types, including Code 128, EAN-13, UPC-A, ITF-14 and GS1-128 |
| Versioning | Draft, published versions, render any version by number, one-call rollback |
| Output | PDF, or PNG/JPEG/WebP from the same template |
| Price | Free: 50 renders/month. Paid from €15/month (billed annually) for 3,000 renders, auto top-ups from €6 per 1,000 |
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.
Test the draft with sample data and a test key, then publish. The template gets a stable ID and a version number.
Each request carries the template ID and your data. The API returns the PDF or a signed link to it.
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.
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.
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.,
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") }}
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 %}
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.
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") }}
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.
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.
"version": 7.POST /v1/templates/{id}/rollback makes an earlier version live
again.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.
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.
Template limits and batch sizes grow with each plan. See template limits per plan.
FAQ
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.
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.
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.
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.
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.
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.
50 renders a month on the free plan, no credit card, auto top-ups on paid plans.
Start free