# Templates

Make templates from Markdown, HTML or an editor document, change and delete them, and render them with JSON or CSV data.

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

A template is a document with fields: places where a value from your data goes. Make one in the [template editor](https://legivel.com/docs/templates), or send Markdown, HTML or an editor document to the API. Then send only the data: the response is the PDF. Every route below is also an [MCP tool](https://legivel.com/docs/mcp#tools).

In a path, `{id}` is the template id or `name:` and the name, as in `/v1/templates/name:Invoice`. A template of another account answers 404.

## List

GET/v1/templates

The account's templates, the most recently saved first. Each entry has its `id`, `name`, `revision` and `fields`: the data paths, the CSV table sources and the JSON list sources the template reads. `limit` is how many templates the plan keeps.

```bash
curl https://api.legivel.com/v1/templates \
  -H "Authorization: Bearer $LEGIVEL_API_KEY"
```

## Create

POST/v1/templates

Saves a template. Send a `name`, unique in the account, and exactly one source:

| Name | Type | Description |
| --- | --- | --- |
| `name`required | string | 1 to 120 characters. |
| `markdown` | string | Markdown to import. Headings, paragraphs, lists, quotes, code and tables become blocks; `{{customer.name}}` becomes a field. |
| `html` | string | HTML to import, as for `markdown`. Inline color, weight, size and alignment stay; scripts, styles and forms are left out. |
| `body` | object | An editor document, as [GET](#read) returns it. A picture box takes a PNG or JPEG as a data URL; the save stores it with the template and names it `asset:<id>`. See [Template format](https://legivel.com/docs/templates/format). |

```bash
curl https://api.legivel.com/v1/templates \
  -H "Authorization: Bearer $LEGIVEL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Offer",
        "markdown": "# Offer {{offer.number}}\n\nDear {{customer.name}},\n\nThank you for your request." }'
```

The answer is 201 with the new template. `left` lists what an import left out, with counts, such as `["2 images"]`: images in Markdown and HTML are not imported. Add one in the editor, or as an [image field](#pictures).

```json
{ "id": "8f0c2c3e-5b1d-4d7e-9a51-0d2b6c1f7a40",
  "name": "Offer",
  "revision": 1,
  "updated_at": "2026-10-14T15:02:11.000Z",
  "fields": { "paths": ["customer.name", "offer.number"], "csv": [], "arrays": [] },
  "thumbnail": false,
  "left": [] }
```

Creating costs no document. A name in use answers 409 `template_name_taken`; a full plan answers 403 `template_limit_reached`. Free keeps 3 templates, Starter 25, Pro 100 and Scale 1,000. Creating, updating and deleting need a verified email address, and an account makes at most 60 such changes a minute (429 `rate_limited` past that). A template is stored encrypted, its name included; the data you render it with is never stored.

## Read

GET/v1/templates/{id}

The template as it is now: `body` is the editor document, `fields` what it reads and `revision` the number to send back with an update.

```bash
curl https://api.legivel.com/v1/templates/name:Offer \
  -H "Authorization: Bearer $LEGIVEL_API_KEY"
```

## Update

PUT/v1/templates/{id}

Replaces the template. Send `base_revision`, the revision you read: when someone saved the template since, the answer is 409 `template_version_conflict` and nothing is saved. What you leave out stays as it was: send one of `body`, `markdown` or `html` to replace the document, `name` to rename it. The template keeps only its current document. To keep a copy first, read the template and create a new one from its `body`.

```bash
curl -X PUT https://api.legivel.com/v1/templates/name:Offer \
  -H "Authorization: Bearer $LEGIVEL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "base_revision": 1,
        "name": "Offer 2026",
        "html": "<h1>Offer {{offer.number}}</h1><p>Dear {{customer.name}},</p>" }'
```

## Delete

DELETE/v1/templates/{id}

Answers 204. The template and its pictures are deleted at once and for good. A render that names it answers 404, and the name is free again. This cannot be undone.

```bash
curl -X DELETE https://api.legivel.com/v1/templates/name:Offer%202026 \
  -H "Authorization: Bearer $LEGIVEL_API_KEY"
```

## Render

POST/v1/templates/{id}/render

Fills the template with the data and answers 200 with `Content-Type: application/pdf`. The page count is in `x-legivel-pages`. A render counts one document toward the period, as a Markdown render does, and the plan's page and time limits apply.

A layout that looks broken is still rendered. Text that does not fit its box or table cell, a box drawn over text and a box past the page edge set `x-legivel-warnings: layout_problem`, and `x-legivel-layout-problems` lists up to ten of them as JSON, each with its `path`, `page` and `message`.

Send `Idempotency-Key` to retry safely: the same key and the same data answer with the first PDF again, marked `x-legivel-replay: true`, at no charge. The same key with other data is refused.

```bash
curl https://api.legivel.com/v1/templates/$TEMPLATE_ID/render \
  -H "Authorization: Bearer $LEGIVEL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: invoice-2026-0147" \
  -d '{ "data": { "invoice": { "number": "2026-0147" } },
        "csv": { "items": "item,qty\nPaper,2\n" } }' \
  -o invoice.pdf
```

- `application/json`: `data` holds the field values; `csv` maps table sources to CSV text.
- `text/csv`: the body is the CSV for the template's first CSV table. This works only for a template that reads nothing else; send JSON or multipart when it also has fields.
- `multipart/form-data`: a `data` part with JSON and one `csv:<source>` part per table.

```bash
curl https://api.legivel.com/v1/templates/$TEMPLATE_ID/render \
  -H "Authorization: Bearer $LEGIVEL_API_KEY" \
  -H "Content-Type: text/csv" \
  --data-binary @items.csv \
  -o items.pdf
```

```bash
curl https://api.legivel.com/v1/templates/$TEMPLATE_ID/render \
  -H "Authorization: Bearer $LEGIVEL_API_KEY" \
  -F 'data={"invoice":{"number":"2026-0147"}}' \
  -F 'csv:items=@items.csv' \
  -o invoice.pdf
```

## Data

`fields.paths` names what the template reads, the values its formulas read included. Each kind of value fills one kind of element:

| Name | Type | Description |
| --- | --- | --- |
| `text`, `number`, `date` | string \| number | A field in the text: `{{customer.name}}` reads `{"customer": {"name": "…"}}`. Numbers, money and dates follow the field's format. |
| `list` | array of objects | A table bound to the list, one body row per object. `fields.arrays` names it; each column reads one key, a formula on the record, or the fields in its cell. |
| `CSV` | text | A table bound to `csv:<name>`. The first row names the columns; comma, semicolon or tab. |
| `image` | string | An image box with a field: the image itself, as a data URL. See [Images](#pictures). |

```json
{ "data": {
    "customer": { "name": "Nordic Print Supplies AB", "since": "2024-05-01" },
    "company": { "logo": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA…" },
    "lines": [
      { "item": "Toner cartridge", "qty": 2, "price": 9.5 },
      { "item": "Paper A4, 500 sheets", "qty": 8, "price": 22.5 }
    ] } }
```

Every field the template reads needs a value; an empty string is a value. A value is text in the PDF: it never becomes a link or a style.

### Images

An image field takes the image itself: a PNG or JPEG file as a base64 data URL, `data:image/png;base64,…` or `data:image/jpeg;base64,…`, of at most 4 MB. The bytes must be the type the URL names. A link (`https://…`) is refused: legivel never fetches a file your data names. The image is scaled into its box.

```bash
# A PNG or JPEG file as a data URL, ready for the data:
echo "data:image/png;base64,$(base64 < logo.png | tr -d '\n')"
```

## Refusals

Data that does not fit the template answers 422 `template_data_invalid`, with one entry in `issues` per problem. Nothing is charged.

```json
{ "code": "template_data_invalid",
  "error": "The data does not fill the template. See issues: each names the path and what is wrong.",
  "issues": [
    { "path": "csv:items", "message": "no CSV named \"items\"" },
    { "path": "company.logo", "message": "an image field takes the image as a data URL (data:image/png;base64,…); links are not fetched" } ] }
```

A source that is not an editor document answers 400 `template_invalid` with the first problem, such as `blocks[3].spans is not a list`; so does a document that names a font the editor does not offer, and Markdown nested more than 32 levels deep. A template over 2 MB answers 413. An unknown id answers 404 `not_found`. Every other refusal is the same as for [POST /v1/render](https://legivel.com/docs/api/render). See [Errors and quota](https://legivel.com/docs/errors).
