Twig templating
Docgen templates are HTML + Twig. Twig is a battle-tested templating language from the Symfony ecosystem with a well-documented security model. Docgen runs every template in a strict sandbox — no PHP function passthrough, no file includes from arbitrary paths, no attribute() magic. Templates run as data.
Syntax basics
<h1>Hello {{ name }}</h1>
{% if subscriber.active %}
<p>Welcome back, {{ subscriber.first_name }}.</p>
{% endif %}
{% for line in invoice.lines %}
<tr>
<td>{{ line.description }}</td>
<td>{{ line.amount|number_format(2) }}</td>
</tr>
{% endfor %}Three core constructs:
{{ expr }}— print an expression, auto-escaped for HTML by default.{% tag %}— control-flow tags likeif,for,set.expr|filter— apply a filter, eg.upper,number_format(2),date("Y-m-d").
What the sandbox allows
Tags — if, for, else, elseif, set, spaceless, apply
Filters — escape / e, raw, length, lower, upper, title, capitalize, trim, join, split, default, number_format, date, replace, striptags, nl2br, first, last, reverse, sort, keys, merge, slice, abs, round, format, url_encode
Functions — range, cycle, max, min, date
Anything not on the list is refused at render time with a clear error message. If you need a filter that's missing, file an issue — we can usually add it after a security review.
Auto-escaping
Output is HTML-escaped by default. To render literal HTML (eg. you've sanitized upstream), use the raw filter:
{{ rich_text|raw }}Be careful — rawbypasses XSS protection. If the rendered document is going to a recipient who didn't supply the data, you almost certainly want auto-escaping on.
Comments
{# This is a Twig comment. It does NOT appear in the rendered output. #}What templates can't do
- Make HTTP requests (the sandbox has no network primitives).
- Read or write files (no
include, nosourcebeyond the template body itself). - Call arbitrary PHP functions.
- Reference other templates — each render is one body, no composition. Composition is on the v2 roadmap.
Size cap
Templates are capped at DOCGEN_TEMPLATE_BODY_MAX_BYTES(default 256 KB). If your template is bigger, you're probably embedding base64 images — upload them via POST /v1/assets (coming in v2) and reference them by ID instead.
Next
Merge-field discovery — how the API tells you which variables your template expects before you submit data.