Templates / Quote
Quote
Offer with validity date, optional positions, and a signature line for acceptance. Same data shape as the invoice, so a won quote becomes an invoice without retyping.
1 page, A4. Rendered in 32 ms. 13 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 |
|---|---|
contact | Lena Hartmann |
currency | EUR |
customer | Northwind Traders |
date | 2026-09-28 |
intro | Thank you for the conversation on Tuesday. As discussed, here is our offer for the redesign of the customer portal. |
items | JSON, 292 chars |
logo | logo.png |
no | Q-2026-118 |
sender | Meridian Studio GmbH |
sender_address | Rosenthaler Str. 41, 10178 Berlin |
terms | 50 % on order, 50 % on delivery. Prices net, plus 19 % VAT. |
total | 19 200.00 |
valid_until | 2026-10-26 |
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 quote.pdf -d @- <<'JSON'
{ "template": "quote.typ",
"inputs": {
"contact": "Lena Hartmann",
"currency": "EUR",
"customer": "Northwind Traders",
"date": "2026-09-28",
"intro": "Thank you for the conversation on Tuesday. As discussed, here is our offer for the redesign of the customer portal.",
"items": "[{\"name\":\"Discovery workshop, 2 days\",\"qty\":\"1\",\"total\":\"3 200.00\"},{\"name\":\"UI design, 4 key flows\",\"qty\":\"1\",\"total\":\"9 600.00\"},{\"name\":\"Design system tokens and components\",\"qty\":\"1\",\"total\":\"6 400.00\"},{\"name\":\"Optional: usability test with 6 participants\",\"qty\":\"1\",\"total\":\"2 800.00\"}]",
"logo": "logo.png",
"no": "Q-2026-118",
"sender": "Meridian Studio GmbH",
"sender_address": "Rosenthaler Str. 41, 10178 Berlin",
"terms": "50 % on order, 50 % on delivery. Prices net, plus 19 % VAT.",
"total": "19 200.00",
"valid_until": "2026-10-26"
} }
JSON
Through MCP, render_document with template: "quote.typ" and the same inputs; get_template or fetch lists the keys.
Source
template.typ, 29 lines
#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))
#set text(font: "Libertinus Serif", size: 10.5pt, fill: rgb("1a1917"))
#grid(columns: (1fr, auto), align: (left, right),
if logo != "" { image(logo, width: 38mm) } else { text(size: 16pt, weight: 600)[#input("sender")] },
[#text(size: 20pt, weight: 600)[Quote] \ #text(fill: luma(110))[#input("no")]])
#v(9mm)
#grid(columns: (1fr, 1fr), gutter: 10mm,
[*#input("customer")* \ Attn. #input("contact")],
[#grid(columns: (auto, 1fr), column-gutter: 6mm, row-gutter: 3pt, [Date], [#input("date")], [Valid until], [#input("valid_until")])])
#v(8mm)
#input("intro")
#v(6mm)
#table(columns: (auto, 1fr, auto, auto), align: (right, left, right, right), inset: (y: 7pt),
stroke: (x, y) => if y == 0 { (bottom: 0.7pt) } else { (bottom: 0.3pt + luma(200)) },
[_Pos._], [_Description_], [_Qty_], [_Price_],
..items.enumerate().map(((i, it)) => (str(i + 1), it.name, it.qty, it.total + " " + cur)).flatten())
#v(4mm)
#align(right)[#text(size: 13pt, weight: 600)[Total #input("total") #cur]]
#v(4mm)
#text(size: 9.5pt, fill: luma(90))[#input("terms")]
#v(1fr)
#grid(columns: (1fr, 1fr), gutter: 20mm,
[#line(length: 100%, stroke: 0.5pt) #text(size: 8.5pt, fill: luma(110))[Place, date]],
[#line(length: 100%, stroke: 0.5pt) #text(size: 8.5pt, fill: luma(110))[Accepted for #input("customer")]])