Building a Swiss QR Bill Package (So You Don't Have To)

A few months ago, I needed to generate a Swiss QR bill for an invoice. A colleague had mentioned Typst in passing, a modern markup-based typesetting system gaining traction as a LaTeX alternative. What began as a simple invoicing solution evolved into an evening project that revealed Typst’s elegance and taught me much about its capabilities.

The Swiss QR Bill Problem

A colleague mentioned Typst to me as a modern alternative to LaTeX for document generation. I’d already read about Zerodha using Typst to generate 1.5 million daily statements in just 25 minutes, proof that it’s probably mature enough for real production workloads beyond academic papers. When I needed to generate a Swiss QR bill for an invoice, I thought this would be the perfect opportunity to learn it properly.

Swiss QR bills are standardized payment slips that replaced the old orange and red payment slips (ESR) back in 2020. The specification is surprisingly detailed: over 70 pages covering everything from exact dimensions (210mm × 105mm) to font choices (only Helvetica, Frutiger, Arial, or Liberation Sans are allowed). The layout defines millimeter-accurate positioning, proper support for both QR-IBAN and regular IBAN, three different reference types (QRR, SCOR, NON), and multi-language support for all Swiss national languages.

When I looked for existing solutions, there weren’t any Typst packages available. Given Switzerland’s invoicing requirements, I figured if I needed this, others probably would too.

Discovering Typst

When my colleague mentioned Typst, I was immediately curious. I’d just finished my master’s thesis with LaTeX. The development experience still stung. Cryptic errors, multi-pass compilation, a 4GB Docker image just for the toolchain. Typst promised markdown-like syntax with LaTeX-quality output and instant compilation. The Swiss QR bill spec, with its precise layout requirements, would be a good test.

Building the Package

What started as an inline function to solve my immediate need became a reusable plugin. With Typst’s package system, I set out to create payqr-swiss. The goal was simple: make it easy to generate compliant Swiss QR bills with just a few lines of code.

Design Decisions

I wanted named parameters to make the code self-documenting:

1#swiss-qr-bill(
2  account: "CH4431999123000889012",
3  creditor-name: "Max Muster & Söhne",
4  amount: 1949.75,
5  currency: "CHF",
6  reference-type: "QRR",
7  reference: "210000000003139471430009017",
8  // ... all required fields
9)

No positional arguments to remember, no consulting docs for parameter order.

I made some wrong assumptions early on. The first version generated standalone QR bills on their own page. A user quickly pointed out that most people need to embed QR bills in existing invoices, not generate them separately. I flipped the default: QR bills are now floating elements you position yourself, with standalone: true available if you actually want a separate page.

Font handling went through a similar evolution. The spec lists four allowed fonts (Helvetica, Frutiger, Arial, Liberation Sans) so I hardcoded that order. Then someone needed to match their corporate identity and couldn’t override it. Now font: "auto" uses spec-compliant defaults, but font: "page" lets you inherit from your document, and you can specify any font directly if you’re willing to deviate from the spec.

Getting It Right

Getting the layout millimeter-perfect was tedious but straightforward. The spec defines exact positions for every element. Typst’s place() function with absolute positioning made this easy:

1#place(
2  dx: 19.5mm,  // 46mm (QR width) / 2 - 7mm / 2
3  dy: -26.5mm, // 46mm (QR height) / 2 + 7mm / 2
4  image("assets/ch_cross_7mm.svg", width: 7mm, height: 7mm)
5)

The spec says “Swiss cross: 7mm × 7mm, centered in QR code.” I did the math, wrote the code, it worked.

The first version left reference formatting to the user. A few weeks after publishing, someone opened a PR implementing automatic formatting: QRR references now format as 2+5×5, SCOR as 4×4.

The QR code generation itself relies on the tiaoma package, which already provides QR code generation in Typst.

Lessons Learned

Building this package taught me quite a bit about the Typst ecosystem.

Spec-driven development is surprisingly pleasant when your tools cooperate. I alternated between reading the 70-page Swiss QR bill specification and Typst’s documentation. I’d never used Typst before this project. Find a requirement in the spec, look up how to do it in Typst, implement it, move to the next requirement. The back-and-forth was tedious, but I spent more time reading than debugging. Typst’s layout primitives mapped cleanly to what the spec demanded. The tedium was in understanding the requirements, not fighting the implementation.

Packages are published through a curated, PR-based workflow. To publish a package, you fork the typst/packages monorepo, add your versioned package directory, and open a PR. A handful of maintainers review every submission. It’s manual work that doesn’t scale easily, but the quality bar is noticeably higher than unmoderated registries. I appreciate that someone actually looks at what is accepted into the ecosystem.

Niche doesn’t mean unused. I figured this was a pretty specialized package. Swiss-specific invoicing for a relatively new typesetting system. I was surprised to see actual usage within weeks of publishing. Turns out there’s a healthy overlap between “people who care about typesetting” and “people who need compliant Swiss invoices.”

Putting It Together

Here is a minimal example generating just the QR bill portion. I’m using A5 landscape here to keep the example compact, but in practice you’d embed this in an A4 invoice:

 1#import "@preview/payqr-swiss:0.4.1": swiss-qr-bill
 2
 3#set page(paper: "a5", margin: 2cm, flipped: true)
 4#set text(font: "Helvetica", size: 11pt)
 5
 6#page[
 7  #place(
 8    bottom + center,
 9    dy: 2cm,
10
11    swiss-qr-bill(
12      account: "CH4431999123000889012",
13      creditor-name: "Beispiel Firma AG",
14      creditor-street: "Musterstrasse",
15      creditor-building: "12",
16      creditor-postal-code: "8000",
17      creditor-city: "Zürich",
18      creditor-country: "CH",
19      amount: 1950.00,
20      currency: "CHF",
21      debtor-name: "Hans Muster",
22      debtor-street: "Beispielweg",
23      debtor-building: "45",
24      debtor-postal-code: "6000",
25      debtor-city: "Luzern",
26      debtor-country: "CH",
27      reference-type: "SCOR",
28      reference: "RF18000000000539007547034",
29      additional-info: "Rechnung 2025-001",
30      language: "de"
31    )
32
33  )
34]

The package handles all formatting, positioning, and QR code generation automatically. The result is a compliant QR bill, all from a single source file and generated with a single convenient function.

Example Invoice with Swiss QR Bill

Try It Yourself

The package is available on Typst’s package registry:

1#import "@preview/payqr-swiss:0.4.1": swiss-qr-bill

The source code is on GitHub under the LGPL-3.0 license. Examples, documentation, and contribution guidelines are all there.

Final Thoughts

What started as “I need a QR bill” became an exploration of modern document creation tools spread across a few evenings and a contribution back to the community. Typst proved to be not just capable but genuinely pleasant to work with.

For anyone looking for nice typesetting with a fast, lean and modern workflow, Typst is worth checking out. And if you’re in Switzerland and need to generate QR bills, well, there’s now a package for that. If you’re using Typst for Swiss invoicing or have ideas for improvements, I’d love to hear from you. PRs are welcome!