Skip to main content
This is the exhaustive reference for the template JSON. For a gentler walk-through, start with Template basics and the guides; come here for the precise contract.
Status: the v1 schema is implemented and live behind both /generate and the playground. It’s pre-1.0 and may still grow (additively); it isn’t frozen as a versioned public contract yet.
Two principles bound the design: no expressions (only {{path}} substitution — compute values in your code) and nodes mirror layout, not meaning (column, row, text, … — not invoice, lineItem). See Template basics.

Request envelope

POST /generate
Authorization: Bearer tipar_live_<key>
Content-Type: application/json

{
  "template": { "page": {  } },
  "data":     {  }
}
Success is 200 application/pdf. Failures are Problem Details.

page

{
  "page": {
    "size": "A4",
    "margin": 30,
    "defaultTextStyle": { "fontSize": 10 },
    "header":  "Node | null",
    "content": "Node",
    "footer":  "Node | null"
  }
}
FieldTypeRequiredNotes
size"A4" · "A3" · "Letter" · "Legal"noDefault A4.
marginnumber (points)noDefault 30. Uniform on all four sides. Must be ≥ 0.
defaultTextStyleStylenoPage-level default. Only fontSize and color propagate; bold/semiBold/italic apply only where set on a node’s own style.
headerNodenoRepeats on every page.
contentNodeyesThe page body.
footerNodenoRepeats on every page.

Nodes

Every node has a "type" discriminator. There are seven types.

column

Vertical stack. → guide: Layout
{ "type": "column", "spacing": 8, "items": ["Node", "Node"] }
FieldTypeRequiredNotes
itemsNode[]yesChildren, top to bottom.
spacingnumbernoGap between successive items.

row

Horizontal stack of sized cells. → guide: Layout
{
  "type": "row",
  "items": [
    { "size": { "kind": "relative", "weight": 1 }, "child": "Node" },
    { "size": { "kind": "constant", "width": 180 }, "child": "Node" }
  ]
}
FieldTypeRequiredNotes
items[].size{kind:"relative",weight} · {kind:"constant",width}yesrelative cells share remaining width by weight; constant cells are width points.
items[].childNodeyesCell content.

text

A single styled string. → guide: Text & styling
{ "type": "text", "value": "Invoice {{invoice.number}}", "style": { "fontSize": 18, "bold": true }, "align": "right" }
FieldTypeRequiredNotes
valuestringyesSupports {{path}} interpolation.
styleStylenoMerged over defaultTextStyle.
align"left" · "center" · "right"noDefault left.

richText

Multi-span text with mixed styling and page-number tokens. → guide: Text & styling
{
  "type": "richText",
  "align": "center",
  "spans": [
    { "value": "Page " },
    { "pageNumber": true },
    { "value": " of " },
    { "totalPages": true }
  ]
}
FieldTypeRequiredNotes
spansSpan[]yesAt least one. Rendered inline, in order.
align"left" · "center" · "right"noDefault left.
A Span is one of:
  • { "value": string, "style"?: Style } — an interpolatable, styled text run.
  • { "pageNumber": true } — the current page number.
  • { "totalPages": true } — the total page count.

spacer

A fixed vertical gap. → guide: Layout
{ "type": "spacer", "size": 12 }
FieldTypeRequiredNotes
sizenumber (points)yesMust be ≥ 0.

image

A base64-embedded image. → guide: Images
{ "type": "image", "base64": "iVBORw0KGgo…", "fit": "fitArea" }
FieldTypeRequiredNotes
base64stringyesRaw base64 bytes, no data: prefix. Renderer sniffs the format (PNG, JPEG, …).
fit"fitArea" · "fitWidth" · "fitHeight"noDefault fitArea.

table

A grid with an optional header and a forEach-driven body. → guide: Tables
{
  "type": "table",
  "columns": [
    { "kind": "relative", "weight": 5 },
    { "kind": "constant", "width": 80 }
  ],
  "header": { "row": ["Cell", "Cell"] },
  "body": { "forEach": "invoice.lines", "row": ["Cell", "Cell"] }
}
FieldTypeRequiredNotes
columnsColumn[]yesAt least one. Each is {kind:"relative",weight} or {kind:"constant",width}.
header.rowCell[]noLength must equal columns.length.
body.forEachstringyesDotted path into data resolving to a JSON array.
body.rowCell[]yesRow template. Length must equal columns.length.
Inside body.row, the current array element binds to item — e.g. {{item.description}}. A Cell is:
{ "child": "Node", "border": { "bottom": 1, "color": "grey.darken1" }, "padding": 4 }
FieldTypeRequiredNotes
childNodeyes
border.bottomnumber (points)noBottom-rule thickness. (Top/left/right deferred.)
border.colorColornoDefault black; drawn only when bottom is set.
paddingnumber (points)noVertical padding inside the cell.

Style

{ "fontSize": 10, "bold": true, "semiBold": false, "italic": false, "color": "#666666" }
All fields optional. A node’s style merges over page.defaultTextStyle. fontSize must be > 0 when present. → guide: Text & styling

Colours

A colour is either:
  • a hex string — #RGB, #RRGGBB, or #AARRGGBB (3, 6, or 8 hex digits; case-insensitive — in the 8-digit form the alpha byte comes first, ARGB), or
  • a dotted palette token (case-insensitive) from QuestPDF’s Material palette — "grey.darken1", "blue.medium", "red.lighten2", "indigo.darken3".
The resolver walks the palette path against QuestPDF’s colour set; an unknown token fails validation with 422. Full palette: the QuestPDF colour reference. → guide: Text & styling

Interpolation

Tokens are {{path.to.value}}; whitespace inside the braces is allowed.
  • Paths resolve against data{{customer.name}}, {{invoice.total}}.
  • Inside table.body.forEach, the element binds to item{{item.unitPrice}}.
  • No array indexing (lines[0].name) and no expressions. Put computed values in data.
  • A missing path fails with 422 and lists every missing path.
→ guide: Template basics

Errors

All failures are RFC 7807 Problem Details (application/problem+json) with a top-level errors array of { code, message }. Full reference: Errors.
StatusWhen
400JSON is malformed, has wrong field types or a missing/unknown node "type", or omits template.
422Well-formed JSON but unusable: structurally invalid (missing page.content, a header/body cell count that doesn’t match columns, an unknown size/enum/colour), too complex (over the 10,000-node cap), an interpolation token with no matching data, or a render failure (an element that can’t fit, a layout that won’t converge, or undecodable content such as a bad base64 image).
500A genuinely unexpected server failure. Render problems are surfaced as 422, so a 500 here is rare and should be treated as a bug.
422 codes:
codeMeaning
template.missing_dataOne per missing data path; the path is in message.
template.‹path›A structural problem at ‹path› (e.g. template.page.content.body.row; the 10,000-node cap reports template.page).
template.too_complexA render-time layout failure — an element can’t fit, or the layout won’t converge.
template.invalid_contentContent the renderer couldn’t process (e.g. an invalid base64 image).
Example (missing data):
{
  "title": "Template references missing data",
  "status": 422,
  "errors": [
    { "code": "template.missing_data", "message": "brand.name" },
    { "code": "template.missing_data", "message": "invoice.number" }
  ]
}
Example (structural):
{
  "title": "Template is structurally invalid",
  "status": 422,
  "errors": [
    { "code": "template.page.content.body.row", "message": "body has 2 cell(s), columns has 3" }
  ]
}

Out of scope (v1)

Not supported, by design — deferred to a later schema, gated by customer demand:
  • QR codes, barcodes, signature fields, charts
  • Custom font upload, URL-sourced images
  • Expressions, math, formatting helpers ({{currency total}})
  • Loops or conditionals outside table.body.forEach
  • Saved/named templates — every request inlines its template
  • Array indexing in paths (lines[0]) and a literal {{ in output