Docs menu
API referenceTemplates
Templates
Make templates from Markdown, HTML or an editor document, change and delete them, and render them with JSON or CSV data.
A template is a document with fields: places where a value from your data goes. Make one in the template editor, 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.
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#
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.
curl https://api.legivel.com/v1/templates \
-H "Authorization: Bearer $LEGIVEL_API_KEY"Create#
Saves a template. Send a name, unique in the account, and exactly one source:
| Name | Type | Description |
|---|---|---|
namerequired | 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 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. |
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.
{ "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#
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.
curl https://api.legivel.com/v1/templates/name:Offer \
-H "Authorization: Bearer $LEGIVEL_API_KEY"Update#
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.
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#
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.
curl -X DELETE https://api.legivel.com/v1/templates/name:Offer%202026 \
-H "Authorization: Bearer $LEGIVEL_API_KEY"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.
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.pdfapplication/json:dataholds the field values;csvmaps 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: adatapart with JSON and onecsv:<source>part per table.
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.pdfcurl 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.pdfData#
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. |
{ "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.
# 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.
{ "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. See Errors and quota.