Templates and the generic layout
Quire has seven built-in layouts: six gallery templates for common business documents, and a generic document layout that renders any JSON object. You don’t have to pick one. Send your data and Quire chooses.
The gallery
| Template | What it renders |
|---|---|
invoice |
Line items, discount, tax, amount-due panel and payment details. Multi-page with a repeating table header. |
receipt |
Proof of payment: amount-paid panel with a PAID badge, payment method, line items and a support note. |
quote |
Quote or estimate with line items grouped into sections, subtotals, terms and a client acceptance block. |
credit-note |
Credit against an earlier invoice: reason, credited items, tax, and how the credit is applied. |
statement |
Statement of account for a period: opening and closing balances, activity with a running balance, optional aging. |
certificate |
Landscape completion or achievement certificate with signatures, seal and credential ID. Always one page. |
The document layout is the seventh. Each gallery page lists the template’s fields, its JSON Schema and a sample. From the terminal, npx quirepdf-cli templates invoice shows the same.
Gallery templates do the arithmetic for you: send qty and unit_price, plus a tax or discount rate such as 0.18, and the template computes subtotals and totals. The statement template computes the running balance from opening_balance.
How auto-detection works
When you don’t name a template, Quire checks your data against each gallery template’s JSON Schema:
- If the data passes one or more schemas, the most specific template wins: the one with the most required top-level fields. Ties go to the name that sorts first.
- If nothing passes, Quire uses the generic
documentlayout. - If a template almost matched (two problems or fewer), the response says so in
x-template-hint, for exampleinvoice (seller is required). Add the missing field and the next render uses the invoice template.
Detection is a schema check, nothing more: it’s deterministic and costs nothing extra. The document layout has no required fields, so it’s never detected; it’s only the fallback, or used when you ask for it with ?template=document.
To check which template your data resolves to without rendering, call /v1/validate with no template. It’s free.
Naming a template (?template=invoice) switches off detection. Your data must then pass that template’s schema, or you get 422 invalid_data with every problem listed.
The generic layout
Here is an order record with no template:
{
"order_id": "ORD-88213",
"date": "2 Oct 2026",
"currency": "€",
"to": { "name": "Lena Fischer", "email": "lena@example.de", "address": ["Torstraße 12", "10119 Berlin"] },
"status": "Shipped",
"carrier": "DHL Paket",
"gift_message": "Happy birthday, Jonas! I hope these keep you warm on the trail this winter. Love, Lena and the whole Fischer family.",
"lines": [
{ "sku": "TS-BLK-M", "name": "Organic tee, black, M", "qty": 2, "price": 29.0, "total": 58.0 },
{ "sku": "SK-GRY-L", "name": "Wool socks, grey, L", "qty": 3, "price": 9.5, "total": 28.5 }
],
"totals": { "subtotal": 86.5, "shipping": 4.9, "total": 91.4 },
"tags": ["gift", "priority"]
}
The result is a one-page A4 document:
- Header: the title “Order” with “ORD-88213” on the right, inferred from
order_id. - To block: Lena Fischer, her two address lines and email.
- Details panel: Date 2 Oct 2026, Status Shipped, Carrier DHL Paket.
- Gift message: a titled paragraph, because the string is longer than 80 characters.
- Lines: a table with SKU, Name, Qty, Price and Total columns. The numeric columns are right-aligned, and Price and Total show as
€29.00,€58.00. - Totals: a key/value grid. Subtotal and Total are money (
€86.50,€91.40); Shipping prints as4.9, because “shipping” isn’t a money word. - Tags: a bullet list.
- Footer: “Order · ORD-88213” and “1 of 1”.
Reserved keys
These top-level keys shape the header and page instead of becoming sections:
| Key | Type | Effect |
|---|---|---|
title |
string | Large heading. Default “Document”. |
subtitle |
string | A muted line under the title |
number |
string or number | Shown at the top right and in the footer |
date |
string | First row of the Details panel. Send it pre-formatted. |
from |
string or object | The sender: name and email appear in the header |
to |
string or object | A “To” block: name, company, address (a string or an array of lines), email |
accent |
#rrggbb |
Brand colour for the top rule and marks |
currency |
up to 4 characters | Turns on money formatting, e.g. "$", "€", "CHF " |
notes |
string | A Notes block at the end |
footer |
string | Replaces the default footer text (title · number) |
accent must be a six-digit hex colour, or the request fails with accent has an invalid format.
No title? Quire looks for the first top-level key ending in _id, _number, _no, _ref or _reference with a string or integer value. invoice_no: "A-17" becomes the title “Invoice” and the number “A-17”, and that key isn’t repeated in the body.
Every other key, in order
Each remaining top-level key becomes part of the body, in the order the keys appear in your JSON:
| Value | Rendered as |
|---|---|
| Short scalar (string of 80 characters or fewer, number, boolean, null) | A row in the Details panel next to the To block |
| String longer than 80 characters | A titled paragraph section |
| Object | A titled section: its scalar fields in a key/value grid, its nested objects and arrays as sub-sections (up to four levels deep) |
| Array of objects | A table. Columns are every key seen, in first-seen order. Numeric columns are right-aligned. The header row repeats on each page. |
| Array of scalars | A bullet list |
| Mixed array | A list of rendered values |
| Empty array | “None” |
Inside a table cell, a nested object or array is summarised on one line, like City: Berlin, Zip: 10119. Tables with more than six columns use smaller text.
Formatting
- Keys are humanised:
parts_usedbecomes “Parts used”. - Numbers get thousands separators (
12,000) and at most two decimals, without trailing zeros (55.2). - Money: when
currencyis set, a number whose key contains amount, price, total, cost, fee, balance, subtotal, tax, rate, charge, payment, paid, discount, refund, salary or budget is shown as money with two decimals (€1,284.37). Aratebelow 1 stays a plain number, so a0.18tax rate isn’t shown as€0.18. - Booleans print as Yes or No. Null prints as “—”.
- No automatic sums. The generic layout never adds up a column or invents a total. If you want a total, send it.
Tips for shaping your JSON
- Key order is section order. Put the most important section first. Most JSON libraries keep insertion order, including
JSON.stringifyand Python’sjson.dumps. - Set
currencywhenever the data has money in it, and name money keys with the words above:shipping_fee, notshipping. - Use
title,fromandtofor a proper letterhead. Without them you get a plain heading. - Pre-format dates and labels. Quire prints strings as given, so send
"2 Oct 2026", not a timestamp. - Use arrays of objects for anything tabular. Keep each row’s keys the same so the columns line up.
- Flatten what you don’t need. Internal IDs and deeply nested metadata become sections too; drop them before sending.
- Read the hint. If
x-template-hintnames a template, a field or two more gets you a purpose-built layout with computed totals.