# Template format

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

Page: https://legivel.com/docs/templates/format

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.

```json
{
  "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" }
  ]
}
```

```json
{ "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

| Name | Type | Description |
| --- | --- | --- |
| `version`required | 0 | The format version. |
| `page.width`, `page.height`required | number | A4 is 595.28 × 841.89, Letter 612 × 792. |
| `page.margins`required | object | `top`, `bottom`, `left`, `right`: the body area. The bands sit in the margins. |
| `page.first.margins` | object | Page 1's own `top` and `bottom`, for a letterhead. |
| `base` | Style | The style every paragraph starts from. |
| `blocks`required | array | The body: paragraphs, tables and graphics, flowing from page to page. |
| `bands` | object | Text repeated on every page. See [Bands](#bands). |
| `placed` | array | Boxes at a fixed place. See [Placed boxes](#placed). |
| `layout` | object | The editor's page grid. See [Grid and label sheets](#grid). |
| `id` | string | Blocks, 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:

| Name | Type | Description |
| --- | --- | --- |
| `text`required | string | The text. Empty for a field, a formula or a page number. A line break (`\n`) starts a new line. |
| `field` | string | A data path such as `customer.name`: the value replaces the text. |
| `formula` | string | An Excel formula such as `SUM(lines.price)`: its value replaces the text. See [Formulas](#formulas). |
| `format` | object | How a field's or a formula's value is written. See [Fields and formats](#fields). |
| `fallback` | string | The 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. |
| `style` | Style | The 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.

| Name | Type | Description |
| --- | --- | --- |
| `font` | string | `sans` (Noto Sans, the default), `serif`, `mono`, or a family of the [font list](#fonts). Another name is refused. |
| `size` | number | Points. |
| `color`, `background` | string | `#rrggbb`. |
| `bold`, `italic`, `underline`, `strike` | boolean | A font without bold or italic faces gets them drawn. |
| `align` | string | `left`, `right`, `center` or `justify`. |
| `lineHeight` | number | A multiple of the size. |
| `spaceBefore`, `spaceAfter`, `indent` | number | Points. |
| `keepWithNext`, `keepTogether`, `breakBefore` | boolean | Page 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.

| Name | Type | Description |
| --- | --- | --- |
| `kind`required | string | `text`, `number`, `currency` or `date`. A date value is `yyyy-mm-dd`. |
| `decimals` | integer | For `number` (default 0) and `currency` (default 2). |
| `currency` | string | An ISO code. EUR, USD and GBP print €, $ and £, RON prints lei; other codes, such as CHF and JPY, print the code. Default EUR. |
| `locale` | string | `en`, `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)`.

| Name | Type | Description |
| --- | --- | --- |
| `repeat.source`required | string | A JSON path to a list of records, or `csv:<name>`. |
| `repeat.span` | integer | The blocks the repeat covers, this one first. Default 1. |
| `repeat.as` | string | The 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.

| Name | Type | Description |
| --- | --- | --- |
| `columns`required | array | Per column, `width` in points or a `weight` that shares the free width. |
| `rows`required | array | Each 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. |
| `headerRepeat` | boolean | Repeat the header rows on each page the table runs onto. |
| `minRowsPerPage` | integer | Rows to keep together at a page break. |
| `tableStyle` | object | `borderColor`, `borderWidth`, `headerBackground`, `bandBackground` (striped rows), `padding`. |
| `bind.source` | string | A JSON path to a list of records, such as `lines`, or `csv:<name>` for CSV data. |
| `bind.columns` | array | Per 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.sort` | object | `by` (a record key) and `desc`: the rows in that order, smallest first unless `desc` is true. |
| `bind.filter` | string | A formula on the row's record: a row shows only when it is true. |
| `style`, `when`, `repeat` | object, string, object | As 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.

| Name | Type | Description |
| --- | --- | --- |
| `type`required | string | `text`, `rect`, `ellipse`, `line`, `image` or `graphic`. |
| `x`, `y`, `width`, `height`required | number | From the top left corner of the page, in points. A line runs from (x, y) to (x + width, y + height). |
| `pages` | string | `all`, `first` or a page number such as `"2"`. Default `first`. |
| `blocks`, `padding` | array, number | A text box's paragraphs and the space around them. |
| `fill`, `stroke`, `strokeWidth` | string, string, number | The box's background and border. |
| `src` | string | An image's file as a PNG or JPEG data URL. |
| `field` | string | An 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](https://legivel.com/docs/api/templates#pictures). |
| `fit` | string | An image: `contain` keeps its proportions inside the box, centered; `fill` (the default) stretches it to the box. |
| `aspect` | number | An image's width divided by its height. With `fit: "contain"`, a data URL gives its own size and needs none. |
| `graphic` | object | A graphic box's content. See [Graphics](#graphics). |
| `when` | string | A formula: the box shows only when it is true. |
| `slot` | string | The grid cell the box fills. See [Grid and label sheets](#grid). |

## 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`.

| Name | Type | Description |
| --- | --- | --- |
| `kind`required | string | `barcode`, `math`, `mermaid`, `svg`, `code`: `mermaid` draws diagrams and charts. |
| `source` | string | The math (LaTeX), diagram (Mermaid), code or SVG text. |
| `lang` | string | Code: the language to highlight. |
| `barcode` | object | `type` (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. |
| `chart` | object | A 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.
