Docs menu

TemplatesTemplate format

Template format

The JSON document behind every template, for building and changing templates in code.

A template is one JSON document. The template editor writes it; GET /v1/templates/{id} and the MCP tool get_template return it as body; you can change it and send it back with PUT or update_template. Markdown and HTML make a template too.

Lengths are in points: 72 points make an inch, 2.835 a millimeter. Keys this reference does not name are ignored.

Example#

An A4 invoice: a title with a field, a due date in the English date format, a table with one row per record of the lines list, a total from a formula, page numbers in the footer and a logo from the data on page 1.

The second block is the data it reads. The logo is shortened here; a real one is the whole base64 text.

{
  "version": 0,
  "page": { "width": 595.28, "height": 841.89,
            "margins": { "top": 72, "bottom": 60, "left": 56, "right": 56 } },
  "base": { "font": "sans", "size": 10.5 },
  "bands": {
    "footer": { "blocks": [ { "spans": [ { "text": "Page " }, { "text": "", "auto": "page" },
                                         { "text": " of " }, { "text": "", "auto": "pages" } ],
                              "style": { "align": "right", "size": 8 } } ] }
  },
  "blocks": [
    { "spans": [ { "text": "Invoice ", "style": { "bold": true, "size": 20 } },
                 { "text": "", "field": "invoice.number", "style": { "bold": true, "size": 20 } } ] },
    { "spans": [ { "text": "Due " },
                 { "text": "", "field": "invoice.due", "format": { "kind": "date", "locale": "en" } } ],
      "style": { "spaceAfter": 12 } },
    { "type": "table", "headerRepeat": true,
      "columns": [ { "weight": 3 }, { "weight": 1 }, { "weight": 1 } ],
      "tableStyle": { "borderColor": "#d2d6dc", "borderWidth": 0.5, "headerBackground": "#eef2f7", "padding": 5 },
      "rows": [
        { "kind": "header", "cells": [ { "spans": [ { "text": "Item" } ] },
                                       { "spans": [ { "text": "Qty" } ] },
                                       { "spans": [ { "text": "Price" } ] } ] },
        { "kind": "body", "cells": [ { "spans": [ { "text": "" } ] },
                                     { "spans": [ { "text": "" } ], "style": { "align": "right" } },
                                     { "spans": [ { "text": "" } ], "style": { "align": "right" } } ] } ],
      "bind": { "source": "lines",
                "columns": [ { "key": "item" }, { "key": "qty", "format": { "kind": "number" } },
                             { "key": "price", "format": { "kind": "currency", "currency": "EUR" } } ] } },
    { "spans": [ { "text": "Total " },
                 { "text": "", "formula": "SUM(lines.price)", "format": { "kind": "currency", "currency": "EUR" } } ],
      "style": { "align": "right", "bold": true } }
  ],
  "placed": [
    { "type": "image", "field": "company.logo", "x": 440, "y": 30, "width": 100, "height": 40, "pages": "first", "fit": "contain" }
  ]
}
{ "invoice": { "number": "2026-0147", "due": "2026-11-13" },
  "company": { "logo": "data:image/png;base64,iVBORw0KGgo…" },
  "lines": [ { "item": "Toner cartridge", "qty": 2, "price": 9.5 } ] }

Page#

NameTypeDescription
versionrequired0The format version.
page.width, page.heightrequirednumberA4 is 595.28 × 841.89, Letter 612 × 792.
page.marginsrequiredobjecttop, bottom, left, right: the body area. The bands sit in the margins.
page.first.marginsobjectPage 1's own top and bottom, for a letterhead.
baseStyleThe style every paragraph starts from.
blocksrequiredarrayThe body: paragraphs, tables and graphics, flowing from page to page.
bandsobjectText repeated on every page. See Bands.
placedarrayBoxes at a fixed place. See Placed boxes.
layoutobjectThe editor's page grid. See Grid and label sheets.
idstringBlocks, rows and placed boxes may carry an id. The editor uses it for its undo steps. It changes nothing in the PDF.

Paragraphs and spans#

A paragraph is {"spans": [...], "style": {...}}; type is paragraph or left out. A span is a run of text with one style:

NameTypeDescription
textrequiredstringThe text. Empty for a field, a formula or a page number. A line break (\n) starts a new line.
fieldstringA data path such as customer.name: the value replaces the text.
formulastringAn Excel formula such as SUM(lines.price): its value replaces the text. See Formulas.
formatobjectHow a field's or a formula's value is written. See Fields and formats.
fallbackstringThe text shown when the data has no value (missing, null or empty). A field with a fallback is optional.
auto"page" | "pages"The page number or the page count. In bands and placed boxes only.
styleStyleThe span's own style, over the paragraph's.

Style#

A paragraph's style sets its layout and its text; a span's style sets its text only.

NameTypeDescription
fontstringsans (Noto Sans, the default), serif, mono, or a family of the font list. Another name is refused.
sizenumberPoints.
color, backgroundstring#rrggbb.
bold, italic, underline, strikebooleanA font without bold or italic faces gets them drawn.
alignstringleft, right, center or justify.
lineHeightnumberA multiple of the size.
spaceBefore, spaceAfter, indentnumberPoints.
keepWithNext, keepTogether, breakBeforebooleanPage breaks: keep the paragraph on the next one's page, keep its lines together, start it on a new page.

Fields and formats#

A field reads a path of the JSON data: invoice.number reads {"invoice": {"number": …}}. Without a format, the value is written as it is.

A field the data does not fill fails the render, unless the span has a fallback.

For a long date, use a formula: TEXT(invoice.date, "mmmm d, yyyy") gives October 28, 2026, and TEXT(invoice.date, "d mmmm yyyy") gives 28 October 2026. dddd writes the weekday and ddd its short name: TEXT(invoice.date, "dddd, mmmm d, yyyy") gives Wednesday, October 28, 2026. d and dd stay the day of the month.

NameTypeDescription
kindrequiredstringtext, number, currency or date. A date value is yyyy-mm-dd.
decimalsintegerFor number (default 0) and currency (default 2).
currencystringAn ISO code. EUR, USD and GBP print €, $ and £, RON prints lei; other codes, such as CHF and JPY, print the code. Default EUR.
localestringen, en-US, de, fr or ro: separators, the currency position and the date pattern.

Formulas and conditions#

A formula is Excel's formula language without the =. A data path is a name: invoice.total * 1.2, SUM(lines.price). A path to a list gives a list, so COUNTA(lines.item) counts the records. TODAY() is the render date.

when on a block or a placed box is a formula: the block or box shows only when it is true. A number is true unless 0; a blank is false.

The functions: ABS, AND, AVERAGE, AVERAGEIF, CEILING, CONCAT, CONCATENATE, COUNT, COUNTA, COUNTBLANK, COUNTIF, COUNTIFS, DATE, DATEDIF, DAY, DAYS, DROP, EDATE, EOMONTH, EXACT, FALSE, FILTER, FIND, FIXED, FLOOR, IF, IFERROR, IFNA, IFS, INDEX, INT, ISBLANK, ISERROR, ISNUMBER, ISTEXT, LEFT, LEN, LOWER, MAX, MID, MIN, MOD, MONTH, NETWORKDAYS, NOT, OR, PI, POWER, PRODUCT, PROPER, REPLACE, REPT, RIGHT, ROUND, ROUNDDOWN, ROUNDUP, ROWS, SEARCH, SIGN, SORT, SQRT, SUBSTITUTE, SUM, SUMIF, SUMIFS, SUMPRODUCT, SWITCH, TAKE, TEXT, TEXTJOIN, TODAY, TRIM, TRUE, TRUNC, UNIQUE, UPPER, VALUE, WEEKDAY, XOR, YEAR.

Repeated blocks#

repeat on a block shows it, and the blocks it covers, once per record of a list. Each item of the list must be a record: a list of plain strings is refused. Write such a list as records ([{"text": "…"}]), or join it in one paragraph with TEXTJOIN(", ", TRUE, skills).

NameTypeDescription
repeat.sourcerequiredstringA JSON path to a list of records, or csv:<name>.
repeat.spanintegerThe blocks the repeat covers, this one first. Default 1.
repeat.asstringThe name the covered blocks read the record by. Default item: a field item.qty reads the record's qty.

Tables#

A table is a block with "type": "table". Its body rows can be written in the template, or come from data with bind: one body row per record, each cell styled as the template's first body row.

NameTypeDescription
columnsrequiredarrayPer column, width in points or a weight that shares the free width.
rowsrequiredarrayEach row has kind (header, body or footer) and cells, and keepWithNext to keep it on the next row's page. A cell has spans, a style, and colSpan and rowSpan for merged cells; a row lists only the cells that start in it.
headerRepeatbooleanRepeat the header rows on each page the table runs onto.
minRowsPerPageintegerRows to keep together at a page break.
tableStyleobjectborderColor, borderWidth, headerBackground, bandBackground (striped rows), padding.
bind.sourcestringA JSON path to a list of records, such as lines, or csv:<name> for CSV data.
bind.columnsarrayPer table column, the record key, or a formula on the row's record (item.qty * item.price), and an optional format. A column with neither ({}) fills its cell of the first body row once per record: the cell's spans keep their own styles, and their fields and formulas read the record as item, such as a bold item.role and a gray item.company in one cell.
bind.sortobjectby (a record key) and desc: the rows in that order, smallest first unless desc is true.
bind.filterstringA formula on the row's record: a row shows only when it is true.
style, when, repeatobject, string, objectAs on a paragraph.

Bands#

bands holds text repeated on every page, in the margins: header, footer, left and right (the side labels: the left one reads upward, the right one downward). Each is {"blocks": [paragraphs]}. bands.first holds page 1's own bands; a band it leaves out falls back to the others. inset and gap set the distance from the page edge and from the body, in points (default 24 and 6).

Placed boxes#

placed lists boxes at a fixed place on the page, over the body: text boxes, colored boxes, ellipses, lines, images and graphics.

NameTypeDescription
typerequiredstringtext, rect, ellipse, line, image or graphic.
x, y, width, heightrequirednumberFrom the top left corner of the page, in points. A line runs from (x, y) to (x + width, y + height).
pagesstringall, first or a page number such as "2". Default first.
blocks, paddingarray, numberA text box's paragraphs and the space around them.
fill, stroke, strokeWidthstring, string, numberThe box's background and border.
srcstringAn image's file as a PNG or JPEG data URL.
fieldstringAn image's data path: the data gives the image, as a PNG or JPEG data URL of at most 4 MB. A link is refused; nothing is fetched. See Images.
fitstringAn image: contain keeps its proportions inside the box, centered; fill (the default) stretches it to the box.
aspectnumberAn image's width divided by its height. With fit: "contain", a data URL gives its own size and needs none.
graphicobjectA graphic box's content. See Graphics.
whenstringA formula: the box shows only when it is true.
slotstringThe grid cell the box fills. See Grid and label sheets.

Graphics#

A graphic is a barcode or QR code, math, a diagram or chart, code or SVG. It sits in the text flow as a block, {"type": "graphic", "graphic": {…}, "width": 120, "height": 120, "align": "center"} (align is left, center or right; the box never splits), or at a fixed place as a placed box of type graphic. In a source or a barcode value, ${path} puts in a field's value and ${=FORMULA} the result of a formula.

The barcode types: qr, microqr, rmqr, ean13, ean8, upca, upce, code128, itf14, itf, code39, code93, codabar, msi, databar, databar-limited, databar-expanded, datamatrix, pdf417, aztec, maxicode.

NameTypeDescription
kindrequiredstringbarcode, math, mermaid, svg, code: mermaid draws diagrams and charts.
sourcestringThe math (LaTeX), diagram (Mermaid), code or SVG text.
langstringCode: the language to highlight.
barcodeobjecttype (one of the barcode types), value, and options such as ec, module-shape, module-color, background, label, gs1. A linear barcode prints its value under the bars: label: none hides it, other label text replaces it, and label-position is below, above, inside-bottom or inside-top. A value with line breaks, as an EPC payment needs, is drawn plain and takes no options.
chartobjectA chart drawn from a list: type (bar, line, pie), list (a JSON path or csv:<name>), label and value (the record keys for the names and the numbers), and an optional title. Each record gives one bar, point or slice; bars and lines start at zero. It replaces source.

Grid and label sheets#

layout is the editor's page grid: inset (points from the page edge), an optional top (points from the page top where the grid starts, below a page header) and a root cell. A cell has an id, and either children side by side (dir: "row") or stacked ("column"), or none. size shares the free space, fixed sets a length in points, gap separates children, and one cell may be body: true: the text flow's area. Placed boxes name their cell in slot; the editor keeps their place and size equal to the cell's.

layout.repeat.source (a JSON path or csv:<name>) makes a label sheet: each cell takes the next record, left to right and top to bottom, and pages repeat until the records run out. A cell with no box of its own repeats the boxes of the first cell that has some. Its boxes read the record as item and its number as index.

Fonts#

sans, serif and mono, and these families: Roboto, Open Sans, Lato, Montserrat, Inter, Poppins, Source Sans 3, Raleway, Nunito, Work Sans, Rubik, IBM Plex Sans, PT Sans, Fira Sans, Barlow, Merriweather, Lora, Playfair Display, PT Serif, Libre Baskerville, EB Garamond, Source Serif 4, Crimson Text, Roboto Slab, Cormorant Garamond, Bitter, IBM Plex Serif, Spectral, Libre Caslon Text, Oswald, Bebas Neue, Anton, Abril Fatface, Archivo Black, Alfa Slab One, Righteous, Lobster, Josefin Sans, Dancing Script, Pacifico, Caveat, Great Vibes, Satisfy, Indie Flower, JetBrains Mono, Fira Code, Source Code Pro, IBM Plex Mono, Roboto Mono, Space Mono.

Limits#

A template is at most 2 MB of JSON, its images included.

The plan sets how many templates an account keeps; GET /v1/templates and list_templates say how many.

The plan's page and time limits apply to each render.