# 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**:

| Snippet                | Where it renders                                                                                                          |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| **Post-invoice block** | Directly under the totals table, before the payment accounts. Typically payment instructions, terms, or a thank-you note. |
| **Footer**             | At the bottom of **every page**, centred and in small type (8pt). Typically a legal notice or registry data.              |

Both snippets are written in [Typst](https://typst.app/docs/), a modern typesetting language. This page covers what you can write in them; the values you can interpolate are covered in [Placeholders](https://docs.uninvoice.app/templates/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 want                                       | Typst reference                                                                                                                                                                    |
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Markup rules (`*bold*`, `_italic_`, lists, escapes) | [Syntax](https://typst.app/docs/reference/syntax/)                                                                                                                                 |
| Size, weight, colour, font                          | [`text`](https://typst.app/docs/reference/text/text/)                                                                                                                              |
| Vertical / horizontal space                         | [`v`](https://typst.app/docs/reference/layout/v/), [`h`](https://typst.app/docs/reference/layout/h/)                                                                               |
| Centre, right-align, indent a block                 | [`align`](https://typst.app/docs/reference/layout/align/), [`block`](https://typst.app/docs/reference/layout/block/)                                                               |
| Columns of content                                  | [`grid`](https://typst.app/docs/reference/layout/grid/)                                                                                                                            |
| A bordered data table                               | [`table`](https://typst.app/docs/reference/model/table/)                                                                                                                           |
| Bullet and numbered lists                           | [`list`](https://typst.app/docs/reference/model/list/), [`enum`](https://typst.app/docs/reference/model/enum/)                                                                     |
| Hyperlinks                                          | [`link`](https://typst.app/docs/reference/model/link/)                                                                                                                             |
| Rules, boxes, colours                               | [`line`](https://typst.app/docs/reference/visualize/line/), [`rect`](https://typst.app/docs/reference/visualize/rect/), [`rgb`](https://typst.app/docs/reference/visualize/color/) |
| Variables, `if`, loops, arrays                      | [Scripting](https://typst.app/docs/reference/scripting/)                                                                                                                           |

## 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](https://docs.uninvoice.app/templates/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 accounts@example.com.] 
```

## 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.
