Templates / Handlebars reference
Handlebars reference
Templates are HTML with Handlebars placeholders. The data you post to the API fills those placeholders. This page is the complete reference for what the engine supports.
Expressions
A double-brace expression inserts a value, HTML-escaped by default. Escaping is what keeps arbitrary data from breaking your markup (or injecting it), so it is almost always what you want.
<h1>Invoice {{number}}</h1>
<p>Billed to {{customer}}</p>To insert a value without escaping (raw HTML), use triple braces:
{{{signatureSvg}}}Triple braces disable escaping. Only use them for content you trust. Never wrap data that
originates from your users in {{{…}}}, or you open an HTML-injection hole in the document.
Nested paths
Reach into nested objects with dot paths:
<p>{{seller.name}} — VAT {{seller.vatId}}</p>Block helpers
Four block helpers are supported.
each — repeat over a list
<table>
{{#each items}}
<tr>
<td>{{description}}</td>
<td>{{amount}}</td>
</tr>
{{/each}}
</table>Inside the block the context is the current item, so {{description}} refers to
item.description. The data for the example above looks like:
{
"items": [
{ "description": "Consulting", "amount": "€900.00" },
{ "description": "Support", "amount": "€340.00" }
]
}Within an each block you also have:
{{@index}}— the zero-based position of the current item.{{@first}}/{{@last}}— booleans, handy withiffor borders or separators.{{this}}— the item itself, useful when the list holds scalars ({{#each tags}}{{this}}{{/each}}).
if / else — conditional content
{{#if notes}}
<section class="notes">{{notes}}</section>
{{else}}
<p>No additional notes.</p>
{{/if}}A value is "falsy" (the if does not render) when it is missing, false, 0, an empty string,
or an empty list.
unless — the inverse of if
{{#unless paid}}
<p class="due">Payment due on receipt.</p>
{{/unless}}with — enter an object's scope
{{#with customer}}
<p>{{name}}</p>
<p>{{address}}</p>
{{/with}}with runs its body once with the given object as context, so you can write {{name}} instead
of {{customer.name}}.
Root context
Inside a block, reach back to the top-level data with @root:
{{#each items}}
<td>{{description}} ({{@root.currency}})</td>
{{/each}}Not supported
To keep rendering predictable, the engine runs standard Handlebars only. The following are not available, and using them fails the render with a clear error rather than rendering something wrong:
- Custom helpers (for example a
{{currency total}}you defined yourself). - Partials (
{{> header}}).
There are no date or number formatting helpers. Format values in your data before sending them, which also keeps formatting (currency symbols, locales, rounding) under your control. See Common recipes.
How variables are inferred
The editor reads your template and infers the variables it expects, which powers autocomplete and validation. Each variable gets a kind:
| Usage | Inferred kind |
|---|---|
{{name}} | scalar |
{{a.b}} (dotted path) | object |
{{#each items}} | list |
{{#with obj}} | object |
{{#if flag}} / {{#unless flag}} | any (presence only) |
A variable used unconditionally is required; one used only inside an if/unless is treated
as optional. The Contract tab renders the full inferred shape as a TypeScript type,
descending into nested object properties and each/with bodies (so items becomes
Array<{ ... }> and billTo.name becomes a nested field). Field types are inferred from your
sample data: a value of "INV-001" types the field as string, 1290 as number, and so
on, widening to string | number | boolean wherever the sample shows nothing. Validation itself
stays top-level: only the top-level keys (is billTo an object, is items a list) are checked
at generation, not the nested fields. So the type documents what your template reads, while a
missing nested field renders as empty rather than failing the call.
How data is validated
Before rendering, your data is checked against the inferred variables. If anything is off, the
API returns invalid_data (422) and lists every problem at once, so you can fix them in one
pass. The problems are:
missing_variable— a required variable has no value in the data.not_a_list— a variable used with{{#each}}is not an array.wrong_type— a scalar got an array or object (or vice versa), which would render as junk.
The same checks run live in the editor,
so a template that previews cleanly will not hit invalid_data in production.