Accounting documents
Invoices, offers, credit notes and the other accounting documents — data in JSON, a short Typst wrapper, schemas, the totals check, Kleinunternehmer and e-invoices.
Templates, not tax advice. The invoice and tax-case profiles are templates. No tax adviser or accountant has reviewed them, and they may need adjusting to your situation and country. Have them checked before you issue real invoices. Examples use fictional companies, accounts and rates.
Ten templates take their content as data: invoice, offer, credit-note, reminder,
delivery-note, order-confirmation, quotation-request, time-sheet, letter and
credentials. The data lives in a JSON file; a four-line .typ file loads it and calls the
template.
The wrapper
#import "@local/business-templates:0.3.0": offer
#offer(
data: json("AN-2026-007.json"),
company: json("/data/company.json"),
)
Every accounting template has the same two parameters:
| Parameter | Content |
|---|---|
data |
the document, a dictionary in the shape of the type’s schema |
company |
company.json, see Branding and languages |
invoice and credentials also take partial locale: overrides, see Languages.
Invoices additionally accept a rendering profile:. For named VAT rates or sales-tax components,
calculated totals, language, currency, paper and layout, use
invoice configuration with typstgen 0.6.2.
The direct wrapper below continues to accept supplied totals, also with typstgen 0.5.0.
Since data and company are ordinary Typst dictionaries, the wrapper can change them before
passing them on — for example to add the logo as bytes:
#let company = json("/data/company.json")
#company.insert("logo", read("/data/logo.png", encoding: none))
#invoice(data: json("RE-2026-001.json"), company: company)
Compile with typstgen compile <file>.typ from the project folder; the PDF lands next to the
source.
Structure of the data
The JSON describes document content; the reusable Typst template determines its layout. The same template can render successive data files without copying the layout for each document.
Deterministic builds
Reproducibility depends on the complete input set: JSON data, template sources, images, font files, compiler/package versions and build time. Keep those fixed for byte-identical rebuilds. Use an explicit timestamp when templates read the current date, and pin the compiler and package revisions rather than relying on whichever versions are installed later.
typstgen compile document.typ --creation-timestamp 1792972800 --manifest archive.json
typstgen verify archive.json
The manifest records the PDF hash, file/font hashes, versions and compile options. Verification rebuilds the document and checks those inputs and the resulting PDF. See the typstgen archiving guide for the full contract. The template’s own document date remains part of its source data.
Document fields
Most types share this outline (the schema of each type has the details and descriptions of every field):
| Key | Content |
|---|---|
metadata |
number, dates, references; show_footer: false hides the company footer |
recipient |
name, company and address for the DIN 5008 address window (supplier for quotation requests, client for time sheets) |
salutation |
greeting and introduction above the table |
items |
positions with description, optional sub_items, quantity, unit, unit_price, vat_rate, total |
totals |
subtotal, vat_breakdown, vat_total, total |
payment |
invoice only: due_date, payment_terms, bank_account |
reminder lists outstanding_invoices instead of items, time-sheet has entries, and
credentials has metadata (title-page data) and services with access data.
letter takes its text as content (plain text) or, for headings, lists and tables, as Typst
content after the call; recipient may be free lines instead of name and address, and final
is a last paragraph kept on one page with the closing:
#letter(
data: (
metadata: (date: "2026-10-08", subject: "Bieterfragen", reference_number: "BRF-2026-001"),
recipient: (lines: ("Muster GmbH", "Herr Max Muster", "Hauptstraße 1", "12345 Musterstadt")),
salutation: "Sehr geehrte Damen und Herren,",
),
company: json("/data/company.json"),
)[
= A. Eignung
…
]
A recipient’s company is left out when it repeats name; the offer prints the company above
the name, the other documents below it. Units tag, monat and jahr take the plural
(Tage, Monate, Jahre) for a quantity other than 1.
Value formats, defined in
common.schema.json:
- Dates as
"2026-03-02"or{ "date": "2026-03-02" }. - Numbers as JSON numbers or strings with a dot (
"1400","71.5"). - Amounts in items and totals as objects:
{ "amount": "1400", "currency": "EUR" }(currency defaults to EUR). The templates read.amount, so a plain number is rejected there. - VAT rates as
{ "code": "Standard", "percentage": "19" }.
The IT consultancy example has one file per type in
examples/it-consultant/documents/ —
the quickest start is to copy one and change the values.
Schemas
Every accounting type and company.json has a JSON schema (draft 2020-12) in
schema/, published at
https://casoon.github.io/typst-business-templates/schema/<name>.schema.json. Reference it from
the file for completion and inline errors in your editor:
{
"$schema": "https://casoon.github.io/typst-business-templates/schema/offer.schema.json",
"metadata": { … }
}
The examples in the repository use relative paths ("../../../../../schema/invoice.schema.json")
instead. Document types links the schema of each type. To
validate in a script, use any JSON Schema validator; the repository’s scripts/check.sh does it
with Python’s jsonschema for every example that names a schema.
Totals check
Invoice, offer, credit note and order confirmation check the numbers before they render anything. The build stops if
- an item’s
totaldiffers fromquantity×unit_price, totals.subtotaldiffers from the sum of the item totals,totals.vat_totaldiffers from the sum ofvat_breakdown,totals.totaldiffers from subtotal minustotals.discount_total(if given) plus VAT,
compared to the cent. All problems are listed at once:
error: panicked with: totals do not add up:
totals.total: 3450 ≠ subtotal + VAT = 3451
Fields that are missing are not checked. The templates never compute a number themselves — the JSON carries every amount, so whatever writes the JSON calculates them.
Kleinunternehmer (§ 19 UStG)
A company that charges no VAT sets two fields in company.json:
"default_terms": {
"vat_exempt": true,
"vat_note": "Gemäß § 19 UStG wird keine Umsatzsteuer berechnet."
}
Invoice and credit note then print vat_note below the totals. The items carry rate 0 with the
code SmallBusiness (or Exempt) and a reason, which the e-invoice needs:
"vat_rate": { "code": "SmallBusiness", "percentage": "0", "exemption_reason": "Kleinunternehmer gemäß § 19 UStG" }
The freelance designer example
is set up this way. With an empty totals.vat_breakdown the totals block shows only the total.
PDF metadata
All accounting documents set the PDF title to the document number and the author to the company
name. All of them except time-sheet, which has no single date, also set the document date
(invoice date, offer date …) as the PDF date, so the output is reproducible and PDF/A-ready.
E-invoice (ZUGFeRD / Factur-X)
typstgen builds an EN 16931 factur-x.xml from the invoice JSON and company.json and embeds it
into a PDF/A-3:
typstgen compile invoice.typ \
--zugferd invoice.json --company data/company.json \
--pdf-standard a-3b
No --creation-timestamp is needed: the template sets the invoice date as document date.
typstgen recomputes the invoice before writing the XML and fails on mismatches, unknown units or
countries and missing exemption reasons. These checks are not a complete EN 16931 validator;
check the result with an external one before sending. Field mapping, unit codes and VAT
categories are in the
typstgen e-invoice guide.
Only invoices are supported — not credit notes. XRechnung and Peppol files are written separately
with typstgen e-invoice (E-invoice files).