Skip to main content

Typst templates

Every invoice PDF uninvoice.app renders is built from a fixed layout (header, recipient, line items, totals, tax breakdown, QR code) plus two snippets you control:

SnippetWhere it renders
Post-invoice blockDirectly under the totals table, before the payment accounts. Typically payment instructions, terms, or a thank-you note.
FooterAt the bottom of every page, centred and in small type (8pt). Typically a legal notice or registry data.

Both snippets are written in Typst, a modern typesetting language. This page covers what you can write in them; the values you can interpolate are covered in Placeholders.

Where to set your snippets

  • Company-wide: the Invoice settings card on the Company page. These apply to every invoice you issue.
  • Per recipient: the invoice template override on a customer's entity form. These apply only to invoices issued to that customer.

The two combine differently:

  • The post-invoice override does not discard the company block. The company's block is handed to your override as a function, #post_invoice_original(), which you call wherever you want it, so a recipient can add a section above or below the standard payment instructions without copying them.
  • The footer override replaces the company footer entirely. If you want to keep the company footer's content, repeat it in the override.
Changes apply to future invoices only

The effective snippets are frozen onto each invoice when it is created, the same way company and recipient details are. Editing a template never changes an invoice you already created. A document that was issued stays exactly as it was rendered.

Typst in one minute

A snippet is ordinary Typst markup: text is text, and # switches into code: a function call, a variable, or a control-flow expression.

Payment is due within 30 days.

*Bank transfer* to #ph("payment_recipient"), quoting #ph("payment_concept").

#text(size: 9pt, fill: gray)[Thank you for your business.]

Useful entry points in the Typst reference:

What you wantTypst reference
Markup rules (*bold*, _italic_, lists, escapes)Syntax
Size, weight, colour, fonttext
Vertical / horizontal spacev, h
Centre, right-align, indent a blockalign, block
Columns of contentgrid
A bordered data tabletable
Bullet and numbered listslist, enum
Hyperlinkslink
Rules, boxes, coloursline, rect, rgb
Variables, if, loops, arraysScripting

Helpers available in your snippets

On top of standard Typst, uninvoice.app defines a small set of helpers bound to the invoice being rendered.

ph("key"): a placeholder value

Returns the value of a built-in or custom placeholder, or an empty string when the invoice carries no value for that key. Because it never fails, optional sections can collapse cleanly:

#if ph("purchase_order") != "" [
Purchase order: #ph("purchase_order")
]

The full list of keys is in Placeholders.

total_numeric: the invoice total as a number

The invoice total in major units (euros, not cents) as a plain number, for arithmetic. Everything else is text.

fx_rate(from, to) and convert(from, to, amount): currency conversion

fx_rate returns the unit rate for a currency pair; convert multiplies an amount by that rate. Both return none when the invoice carries no rate for the pair, so guard the block:

#let usd = convert("EUR", "USD", total_numeric)
#if usd != none [
Equivalent: #calc.round(usd, digits: 2) USD
(1 EUR = #fx_rate("EUR", "USD") USD)
]
Currency codes must be string literals

uninvoice.app scans your snippets for these calls before rendering in order to fetch the rates and freeze them onto the invoice. Only literal codes are found: convert("EUR", "USD", …) works, convert(base, quote, …) does not: computed codes silently resolve to none.

Rates come from the European Central Bank's reference rates, which cover roughly 30 currencies. A rate can be entered by hand when creating the invoice; anything still missing is fetched when the invoice is issued and frozen onto it, so a rendered conversion never moves afterwards. If a required pair cannot be resolved (an unsupported currency, or the rate provider being unreachable), issuing fails and the invoice stays a draft rather than rendering a blank amount.

post_invoice_original(): the company's post-invoice block

Only meaningful in a per-recipient post-invoice override: it renders the company-wide block. Elsewhere it renders nothing, so calling it is always safe.

This invoice is covered by framework agreement #ph("agreement_ref").

#post_invoice_original()

#text(size: 8pt)[Send remittance advice to [email protected].]

What a snippet cannot do

The renderer is deliberately sealed: the same input always produces the same PDF, on any machine.

  • No external packages or imports. #import "@preview/…" and #include of other files are not available.
  • No external files. Images, fonts, or data loaded from disk or a URL are not available; only the invoice's own data is.
  • Fonts are the bundled ones: Helvetica Neue (the document default), Ubuntu, and Ubuntu Mono. Any other family falls back.
  • No "today". datetime.today() is pinned to a fixed date so rendering is reproducible; use ph("invoice_created_at") for the invoice's date.
  • A snippet that does not compile breaks the PDF for every invoice that carries it. Since snippets are frozen per invoice, a broken template affects invoices created while it was in place, not the ones created before.
Preview before you rely on it

Create a draft invoice for the recipient and download its PDF. Drafts render through exactly the same pipeline (with a DRAFT watermark), so it is the fastest way to check a snippet compiles and looks right.

Where snippets do not render

The post-invoice block is skipped on proformas and on invoices whose payment details are suppressed (for example an invoice already collected by card), since both would otherwise ask for a payment that must not be made. The footer renders on every document.