Templates / Invoice
Invoice
Sender block, addressee, line items from JSON, totals with VAT, payment footer. The document most APIs are bought for.
1 page, A4. Rendered in 81 ms. 15 inputs, all optional.
Inputs
Everything variable comes in through sys.inputs. Missing keys fall back to sensible defaults, so an agent can send only what it knows.
| Key | Sample value |
|---|---|
bank | Berliner Volksbank, IBAN DE12 1009 0000 1234 5678 90 |
currency | EUR |
customer | Northwind Traders |
customer_address | Kastanienallee 7, 10435 Berlin |
date | 2026-09-28 |
due | 2026-10-28 |
items | JSON, 240 chars |
logo | logo.png |
no | 2026-0042 |
sender | Meridian Studio GmbH |
sender_address | Rosenthaler Str. 41, 10178 Berlin |
subtotal | 7 140.00 |
total | 8 496.60 |
vat | 1 356.60 |
vat_rate | 19 |
Render it
After copying the template into your account:
curl -X POST https://api.foliant.dev/v1/render \
-H "Authorization: Bearer $FOLIANT_API_KEY" \
-H "Content-Type: application/json" \
-o invoice.pdf -d @- <<'JSON'
{ "template": "invoice.typ",
"inputs": {
"bank": "Berliner Volksbank, IBAN DE12 1009 0000 1234 5678 90",
"currency": "EUR",
"customer": "Northwind Traders",
"customer_address": "Kastanienallee 7, 10435 Berlin",
"date": "2026-09-28",
"due": "2026-10-28",
"items": "[{\"name\":\"Design system audit\",\"qty\":\"1\",\"unit\":\"2 400.00\",\"total\":\"2 400.00\"},{\"name\":\"Component library, phase 1\",\"qty\":\"32 h\",\"unit\":\"120.00\",\"total\":\"3 840.00\"},{\"name\":\"Accessibility review\",\"qty\":\"1\",\"unit\":\"900.00\",\"total\":\"900.00\"}]",
"logo": "logo.png",
"no": "2026-0042",
"sender": "Meridian Studio GmbH",
"sender_address": "Rosenthaler Str. 41, 10178 Berlin",
"subtotal": "7 140.00",
"total": "8 496.60",
"vat": "1 356.60",
"vat_rate": "19"
} }
JSON
Through MCP, render_document with template: "invoice.typ" and the same inputs; get_template or fetch lists the keys.
Source
template.typ, 33 lines
// Invoice. Data comes in through sys.inputs; line items as a JSON string.
#let input(k, default: "") = sys.inputs.at(k, default: default)
#let items = json(bytes(input("items", default: "[]")))
#let cur = input("currency", default: "EUR")
#let logo = input("logo")
#set page(paper: "a4", margin: (top: 22mm, bottom: 24mm, x: 22mm),
footer: context [#set text(size: 8.5pt, fill: luma(110)); #input("sender") · #input("sender_address") #h(1fr) #counter(page).display() / #counter(page).final().first()])
#set text(font: "Libertinus Serif", size: 10.5pt, fill: rgb("1a1917"))
#grid(columns: (1fr, auto), align: (left + top, right + top),
if logo != "" { image(logo, width: 38mm) } else { text(size: 16pt, weight: 600)[#input("sender")] },
[#text(size: 20pt, weight: 600)[Invoice] \ #text(fill: luma(110))[No. #input("no", default: "0001")]])
#v(10mm)
#grid(columns: (1fr, 1fr), gutter: 10mm,
[#text(size: 8.5pt, fill: luma(110), tracking: 0.6pt)[BILL TO] \ *#input("customer", default: "Customer")* \ #input("customer_address")],
[#text(size: 8.5pt, fill: luma(110), tracking: 0.6pt)[DETAILS] \
#grid(columns: (auto, 1fr), column-gutter: 6mm, row-gutter: 3pt,
[Date], [#input("date")], [Due], [#input("due")], [Reference], [#input("no")])])
#v(10mm)
#table(columns: (1fr, auto, auto, auto), align: (left, right, right, right), inset: (y: 7pt),
stroke: (x, y) => if y == 0 { (bottom: 0.7pt) } else { (bottom: 0.3pt + luma(200)) },
[_Item_], [_Qty_], [_Unit_], [_Total_],
..items.map(i => (i.name, i.qty, i.unit, i.total)).flatten())
#v(6mm)
#align(right)[#grid(columns: (auto, 34mm), align: (left, right), row-gutter: 6pt,
[Subtotal], [#input("subtotal") #cur],
[VAT #input("vat_rate", default: "0") %], [#input("vat") #cur],
[#line(length: 100%, stroke: 0.7pt)], [#line(length: 100%, stroke: 0.7pt)],
[*Total due*], [*#input("total") #cur*])]
#v(1fr)
#text(size: 9pt, fill: luma(110))[Payable within 30 days. #input("bank"). Reference #input("no").]