Foliant

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.

Download .typ Open preview

Rendered Quote template, first page
First page, rendered from the source below with the sample inputs.

Inputs

Everything variable comes in through sys.inputs. Missing keys fall back to sensible defaults, so an agent can send only what it knows.

KeySample value
contactLena Hartmann
currencyEUR
customerNorthwind Traders
date2026-09-28
introThank you for the conversation on Tuesday. As discussed, here is our offer for the redesign of the customer portal.
items
JSON, 292 chars
[
  {
    "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"
  }
]
logologo.png
noQ-2026-118
senderMeridian Studio GmbH
sender_addressRosenthaler Str. 41, 10178 Berlin
terms50 % on order, 50 % on delivery. Prices net, plus 19 % VAT.
total19 200.00
valid_until2026-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")]])