Skip to main content

Merge fields & Twig syntax

Anywhere you can type text that ends up on the PDF — Text components, HTML Text components, translation strings, the Details Box, image link URLs, QR code contents — you can include merge fields. These are placeholders that get replaced with real invoice, client, or company data at render time.

BestPDF uses Twig under the hood, which means you can do more than just drop in a value: you can format it, run conditional logic, and loop over lists.

The simple case: dropping in a value​

Wrap a field name in double curly braces:

Invoice {{ invoice.num }} — total {{ invoice.total }}

In the editor, you don't have to type those names from memory: every text field has a green + Translation dropdown and a blue + Merge Field dropdown that insert the correct syntax for you.

Concepts - Merge fields

Available fields​

Invoice designs and quote designs see different fields

An invoice design exposes the invoice.* group; a quote design exposes quote.* instead. The client.* and company.* groups are available in both. The + Merge Field dropdown only ever offers you the group that applies to the design you're editing.

Invoice (invoice designs only)​

FieldWhat it is
invoice.idInternal ID
invoice.numInvoice number (the one shown to the client)
invoice.statuspaid, unpaid, cancelled, draft, etc.
invoice.date_createdIssue date, formatted per the date format setting
invoice.date_dueDue date
invoice.date_paidPayment date (empty if unpaid)
invoice.subtotalSubtotal, formatted with currency
invoice.taxTotal tax amount, formatted with currency
invoice.tax_rateThe tax rate applied, formatted
invoice.totalTotal, formatted with currency
invoice.creditCredit applied to this invoice
invoice.paidAmount already paid
invoice.balanceOutstanding balance
invoice.credit_balanceThe client's remaining credit balance
invoice.account_balanceThe client's running account balance
invoice.notesFree-text notes attached to the invoice
invoice.payment_methodPayment method name
invoice.view_urlDirect link to this invoice in the client area
invoice.eu_reverse_chargeBoolean — true when EU reverse charge applies (see below)
invoice.overdueOverdue reminder level, 0–3 (see below)

Quote (quote designs only)​

FieldWhat it is
quote.idInternal ID
quote.numQuote number
quote.subjectThe quote's subject line
quote.proposalProposal body text — see the note below
quote.statusThe quote's current stage
quote.date_createdIssue date
quote.valid_untilExpiry date
quote.subtotalSubtotal, formatted with currency
quote.totalTotal, formatted with currency

Client​

FieldWhat it is
client.idClient ID
client.nameFull name (first + last)
client.firstnameFirst name
client.lastnameLast name
client.companyCompany name
client.emailEmail address
client.address1, client.address2Address lines
client.city, client.state, client.postcode, client.countryAddress parts
client.country_codeCountry as a two-letter ISO code (CH, DE, …)
client.tax_idVAT / tax number
client.area_urlLink to the client area login page
client.group_idThe client group's ID
client.group_nameThe client group's name
client.signup_dateWhen the client signed up, formatted
client.custom_fields['…']A client custom field, by name

Company​

FieldWhat it is
company.nameYour company name
company.addressYour company address, with the line breaks you configured (see below)
company.urlYour website
company.countryYour country
company.country_codeYour country as a two-letter ISO code
company.tax_codeYour own tax / VAT number
company.tax_id_labelThe label Blesta uses for a tax number, in the document's language

System​

FieldWhat it is
system.urlThe URL of your Blesta installation
system.linkThe same URL wrapped in an <a href> — for HTML Text blocks

Client custom fields​

Custom fields are keyed by their name, not by an internal id, so a template reads the way the admin area does:

{{ client.custom_fields['VAT Number'] }}

The name is the one shown in Blesta's client custom-field settings. A name that doesn't exist renders empty rather than failing, so a template stays safe when a field is removed.

The + Merge Field dropdown inserts client.custom_fields[''] with the quotes already in place — type the field name between them.

The overdue reminder level​

invoice.overdue is 0 for an invoice that isn't overdue, and 1, 2 or 3 once the first, second or third reminder threshold has passed — the thresholds Blesta itself uses for overdue notices. It's the number to branch on when the wording should escalate:

{% if invoice.overdue > 0 %}
{{ lang.overdueNotice[invoice.overdue] }} — {{ lang.overdueMessage[invoice.overdue] }}
{% endif %}

The built-in overdueNotice and overdueMessage strings are indexed by exactly this number, which is what makes the one-liner above work; edit their wording per level on the Translations tab.

Multi-line company address​

company.address is printed with the line breaks you configured in Blesta rather than being flattened onto one line. In an HTML Text block, pipe it through nl2br to keep those breaks visible in the PDF:

{{ company.address|nl2br }}

In a plain Text component the breaks are honoured as they are.

EU reverse charge​

invoice.eu_reverse_charge is a boolean, not a piece of text — it is true when the client is in an EU country other than your own and has a VAT number on file. Test it and print your own wording, or the built-in translated notice:

{% if invoice.eu_reverse_charge %}{{ lang.reversecharge }}{% endif %}

It also works as a condition on any component: the conditions evaluator casts it to 1 / empty, so is not empty means "reverse charge applies". That's the tidy way to show a whole notice block only on reverse-charge invoices.

quote.proposal on Blesta

quote.proposal is offered on quote designs for compatibility with designs carried over from other installations. Blesta quotations carry no separate proposal body, so on Blesta the field currently resolves to empty — use quote.subject, or an HTML Text component, for the covering text.

Twig features you can use​

The template engine runs in a sandbox — most Twig is available, but a few unsafe things (like calling arbitrary PHP) are blocked.

Tags​

  • {% if … %} … {% else %} … {% endif %} — conditional rendering
  • {% for item in list %} … {% endfor %} — loops
  • {% set name = value %} — assign a local variable

Filters​

Filters transform the value, written with a pipe: {{ value|filter }}.

FilterPurpose
upper, lower, capitalize, titleChange case
trimRemove leading/trailing whitespace
number_format(2, '.', ',')Format a number with decimals / separators
date('Y-m-d')Format a date
default('fallback')Use a fallback when the value is empty
lengthLength of a string or list
join(', ')Join a list with a separator
split(',')Split a string into a list
replace({'old':'new'})Find-and-replace
nl2brConvert newlines to <br> (HTML Text only)
abs, roundNumeric helpers
first, last, sliceList access
escape, rawControl HTML escaping

Examples​

Dear {{ client.firstname|default('customer') }},
{% if invoice.status == 'paid' %}
Thank you for your payment.
{% else %}
Please settle the outstanding balance of {{ invoice.balance }} by {{ invoice.date_due }}.
{% endif %}
{{ client.company|upper }}

Translation strings​

Two families of translated text are available alongside the data fields, both offered by the green + Translation dropdown.

BestPDF strings — lang.*​

Everything BestPDF prints itself: column headings, summary row labels, details-box labels, status names. Override the wording per language on the Translations tab, and add your own custom keys there too:

{{ lang.summary_balance }}
{{ lang.footer_disclaimer }}

Blesta's own strings — syslang.*​

syslang reaches straight into Blesta's language files, so any string Blesta itself defines is available in the client's language:

{{ syslang.some_blesta_key }}

Replace some_blesta_key with the key you want. This is an escape hatch for wording you'd otherwise have to duplicate — most designs never need it.

Legacy syntax​

If you're upgrading an older design (or pasting in examples from older docs), you may see the old {$field_name} or {field_name} syntax — for example {$invoice_num} instead of {{ invoice.num }}.

BestPDF still understands these forms and converts them on the fly. You don't have to migrate by hand, but new content should use the Twig syntax for clarity and to access filters and conditionals.

Errors​

If your template has a syntax error, BestPDF won't break — it falls back to printing the original (unrendered) text. This makes broken merge fields easy to spot in the PDF: you'll see literal {{ something }} instead of the expected value.