RistmarkStart free

Documentation

Generating with the /v1 API

Generate documents programmatically with a keyed REST API. Full reference: Swagger UI.

1. Create an API key

Settings → API Keys → Create. Copy the plk_live_… key when it’s shown — it isn’t displayed again. Keyed usage is billed to your plan.

What a key can do. A key is scoped to /v1 and nothing else: generate documents, list your approved templates, and export an approved template’s definition together with the images that template uses. It cannot write anything — no editing, creating or deleting templates, assets, usage or billing — and it cannot create or revoke keys; those need a signed-in session. So a key sitting in a CI secret or a Zapier connection is a generation-and-read credential, not a workspace login. A leaked one can spend your generation allowance and read your approved templates and their images until you revoke it.

2. Authenticate

Send the key as a Bearer token or the x-api-key header:

Authorization: Bearer plk_live_xxx
# or
x-api-key: plk_live_xxx

3. Generate a document

POST /v1/documents with a template id and the data to bind:

curl -X POST https://api.ristmark.app/v1/documents \
  -H "Authorization: Bearer plk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "templateId": "<id>", "format": "pdf", "data": { "invoiceNo": "NW-2026-0184" } }'

Returns the rendered document bytes. format is pdf (default), html or docx. Omit data (or pass null) to render the template’s sample data — the response then carries a sample-data-used warning (see below) so you can tell a real render from a demo one. Only approved templates are generatable via the API.

Don’t know which keys a template’s data object expects? Export it first — the exported bundle carries every $data binding key the template actually reads, so you learn the shape from the template itself rather than guessing from a rendered sample.

strict: true refuses the render outright — 422 render_unresolved, whose diagnostics array lists exactly what refused it — instead of returning a best-effort document when the render carried an error: a value missing from data, a list that is not a list or lacks a table’s columns, a broken expression, a block or fixed-height header too big for its space, a missing picture or letterhead, and the rest marked refuses in Render warnings. Notes never refuse: an empty list (a table prints its header row only) or a flowing header that grew to fit still returns the document, with the note in X-Ristmark-Render-Warnings. Default false: every existing integration keeps getting its best-effort render unless it opts in.

curl -X POST https://api.ristmark.app/v1/documents \
  -H "Authorization: Bearer plk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{ "templateId": "<id>", "format": "pdf", "data": { "invoiceNo": "NW-2026-0184" }, "strict": true }'

Money and dates. A field the template recognized as money or a date is bound to the template’s own format(...) / formatDate(...), not a bare value — the template formats it, not your caller. Pass a plain number for a money field (e.g. 1234.5) and an ISO date (YYYY-MM-DD) for a date field. Migration binds a money field to format(value, 'money'): it renders in the template’s own locale and currency, with that currency’s symbol and decimal places (1234.5 → €1,234.50 in an en-GB, EUR template), or in the currency a string names by ISO code at its start or end (USD 1,234.50). A string it cannot read as an amount, or whose currency symbol does not match that currency ($1,234.50 in a EUR template), is printed exactly as sent, unformatted — and the render says so: money-unformatted:N in the X-Ristmark-Render-Warnings header, and a strict: true render is refused (see Render warnings). A money field bound to plain format(value) — how earlier migrations bound it — renders a locale-formatted number with at most 2 decimal places and no currency symbol, unless a string names an ISO currency code, which renders in that currency. In a string, a single comma or dot followed by exactly three digits, with no other grouping, is a thousands separator in every locale (1,140 is 1140, €1.234 is 1234); a bare 1.234 is the number 1.234. A date field renders in the template’s own locale. A date with a month name (3 October 2026, 3. Oktober 2026, 2026. gada 3. oktobris) and an ISO date (2026-10-03) read the same in every locale, and a date-time with no zone (2026-10-03T00:00:00) is the day it says, wherever it is rendered; a numeric date (03/10/2026) is read day-first, or month-first only when the template’s locale is one that writes it that way (en-US) — the template’s locale decides, not the language a $t rendering is produced in. A value that is not a date that exists (2026-02-30), a month with no day (October 2026), or a two-digit year (03/10/26, never guessed) is printed exactly as sent and the render says so: invalid-date:N in the X-Ristmark-Render-Warnings header, and a strict: true render is refused.

Expression functions

format/formatDate above are two of the functions available inside any {{ }} binding in the template editor — the same set the editor’s own binding autocomplete suggests to whoever builds the template. Documented here because your data is what those bindings evaluate against, so the shape you send interacts with them directly.

Function Signature Example Result
format format(value, kind?) — kind is 'number' (default), 'currency', 'money', 'percent', 'integer' or 'decimal'; uses the template’s own locale/currency. {{ format(invoice.total, 'currency') }} €1,234.50
formatDate formatDate(value, pattern?) — pattern is 'medium' (default), 'short', 'long' or 'iso'; resolved in UTC. {{ formatDate(invoice.issued, 'iso') }} 2026-06-17
default default(value, fallback) — fallback when value is null/undefined/NaN/''. {{ default(customer.phone, '—') }} — when no phone was sent
round round(n, decimals?) — rounds to decimals places (default 0), a half upward, on the number as written: round(1.005, 2) is 1.01, and float noise from earlier arithmetic (1842.5 * 0.19 is stored as 350.0749999…) does not cost a cent. {{ round(item.weightKg, 1) }} 12.3
sum sum(array, path?) — sums a numeric array, or a path plucked from each row; a row with no number there adds nothing, and anything that is not a list sums to 0. {{ sum(lineItems, 'amount') }} 269.5
flatten flatten(array, path?) — joins the lists found at path in each row into one list, in order (with no path, joins rows that are themselves lists); anything that is not a list is skipped, so bad input gives an empty list. Use it for a grand total across grouped data. {{ sum(flatten(groups, 'lines'), 'amount') }} 21.5 for groups holding lines of 10, 5.5, 1, 2 and 3
upper upper(s) — uppercase; null/undefined → ''. {{ upper(customer.countryCode) }} LV
lower lower(s) — lowercase; null/undefined → ''. {{ lower(customer.email) }} jane@example.com
concat concat(...parts) — joins any number of values into one string; null/undefined parts → ''. {{ concat(customer.firstName, ' ', customer.lastName) }} Jane Doe

Every function above degrades gracefully on bad input (blank/0/unresolved, never a thrown error) — a broken expression shows up as expression-error:N in X-Ristmark-Render-Warnings (best-effort mode) or in strict: true’s diagnostics, not a failed request.

4. Discover templates

GET /v1/templates lists the caller’s approved templates only — in_review (freshly migrated, unconfirmed) templates never appear here, matching the trust fence on generation itself:

curl https://api.ristmark.app/v1/templates \
  -H "Authorization: Bearer plk_live_xxx"
{
  "items": [{ "id": "<id>", "name": "Invoice", "latestVersion": 3 }],
  "nextCursor": "100"
}

Each item is { id, name, latestVersion? }. The response is paginated: ?limit= (default 100, capped at 500) and ?cursor= (opaque — pass back the previous page’s nextCursor) walk a large template list one page at a time; nextCursor is absent on the last page. This route is read-only discovery — it isn’t metered.

5. Export a template

GET /v1/templates/{id}/export returns the template itself — the reusable artifact, not a rendered output of it:

curl -OJ https://api.ristmark.app/v1/templates/<id>/export \
  -H "Authorization: Bearer plk_live_xxx"

The response is a ristmark.template.v1 envelope: the template’s canonical JSON (blocks, field definitions, $data bindings, $t translations, width tokens, locale and currency) plus the images and letterheads it references, base64-embedded, so the bundle stands on its own. Digests are computed over the bytes the response carries, so it verifies itself.

Two lists tell you where a bundle is not complete, rather than leaving you to find out: unresolvedAssetIds names an asset the template prints that this workspace could not produce bytes for, and dataBoundImageSrc names any image whose picture comes from data — a dataRef (a picture “from data”) or a src that is a {{ }} binding — listing the expression verbatim: that picture is chosen by the data you generate with, so no export can enumerate it. The bundle carries every picture the template’s own sample data, saved scenarios and translations name — each repeated record’s sample picture included — so every preview of it prints complete; bring the rest yourself. (A UUID-shaped sample value that no picture reads directly and no asset has is not listed as unresolved: it may be an order number rather than a picture.) Both are empty for the ordinary template.

?version=<n> exports a published revision; omit it for the current working copy — the same meaning version has on POST /v1/documents. Approved templates only, same trust fence as generation.

If a template’s assets are too large to embed in one response, the route refuses with 413 and names ?assets=omit, which returns the same template JSON plus an asset manifest (ids, MIME types, sizes, SHA-256 digests) without the bytes.

Export is never metered and never blocked. Taking your own templates out does not consume your generation allowance, does not require credits, and keeps working after you cancel. We will not charge you to retrieve work you already paid to create. (Rate limits apply, as on every route.)

There is no import endpoint yet: re-creating an exported template in a Ristmark workspace today means posting its template object to the signed-in workspace API and re-uploading its assets.

6. Generate in bulk

POST /v1/documents/batch with up to 25 items, each { templateId, data, format, version? } — version pins that item to a published revision, the same meaning it has on POST /v1/documents; omitted, it renders the current working copy — each item is metered by rendered pages — ½ credit per page, counted per page, exactly like a manual render (see Pricing & limits). A response looks like:

{
  "requested": 2,
  "succeeded": 2,
  "results": [
    { "index": 0, "status": "ok", "format": "pdf", "document": "<base64>" },
    { "index": 1, "status": "ok", "format": "pdf", "document": "<base64>" }
  ]
}

Partial success is the contract: one bad item never fails the others — a failed item comes back as { "status": "error", "error": { "code", "message" } } instead of stopping the batch. Batch warnings ride per-item JSON (each result’s warnings array), not the X-Ristmark-Render-Warnings header — a single HTTP response covers up to 25 items, so there’s no one header to carry a per-item signal on. strict is single-document only — a batch item always returns its best-effort render; check its own warnings for the codes marked refuses in Render warnings instead.

Every request is bounded to a 45s timeout. If it fires mid-batch, the whole request comes back 408 — items already billed before the timeout keep their result, but nothing after that point is billed (bytes that never reached you are never charged for).

Have many rows of tabular data instead of a hand-built JSON array? Generate from a spreadsheet covers the CSV/Excel upload path (up to 200 rows) instead.

7. Limits & errors

Status Code Meaning
400 — Invalid request: bad UUID, bad format, an empty/over-cap batch, an unrecognised ?assets= value.
402 quota_exceeded Plan inactive, or the generation fair-use page-step’s extra-unit reserve was refused.
404 — Template not found in your workspace, or not an approved, live template.
408 render_timeout The request exceeded the 45s timeout — aborted, never billed.
413 — The request body (or, on export, the assets) exceeded the size limit — for export, retry with ?assets=omit.
422 render_unresolved strict: true and the render carried an error — the body’s diagnostics array names each one. Notes (an empty list, a header that grew) never refuse; see Render warnings.
422 — The template produced no visible pages for the given data (every section hidden by its own visibility condition).
422 image-limit-exceeded The pictures data names exceed a per-document limit: more than 200 different pictures, more than 64 MB of them, or an inline data: picture over 1 MB. The body’s limit and max name which one. Never billed; a batch reports it per item under the same code.
429 — Rate limited — see the throttle numbers below.
  • A degraded-but-still-usable render (e.g. a DOCX that dropped its letterhead, or a substituted font) is reported in the X-Ristmark-Render-Warnings response header — the document still returns; it’s a signal to look closer, not a failure. Counted codes such as unresolved-bindings:N ride the same header in best-effort mode; Render warnings lists every code, and which ones strict: true refuses instead.
  • language-fallback rides the same warning channel when the language you asked for (say de-CH) is not in the template but its bare language (de) is: the response’s Content-Language header names the language actually rendered. Lookup is exact code first, then the code with its region removed; a different region (de-AT for de-CH) is never used, and a bare de is never answered by de-CH.
  • sample-data-used rides the same warning channel when data was omitted/null and the render fell back to the template’s sample data — worth checking for if you expected a real render.
  • Rate limits (per tenant, per 60s window): GET /v1/templates sits on the default 240/min; POST /v1/documents, POST /v1/documents/batch and GET /v1/templates/{id}/export are tighter at 30/min — the generate routes so a keyed integration’s traffic never eats the same headroom as your team’s own in-app usage, and export because it’s the one un-metered route on this surface, so the rate limit is what bounds it. Read straight from apps/api/src/throttling/rate-limit.module.ts/throttle.config.ts — env-overridable, these are the shipped defaults.
  • See Pricing & limits for per-plan generation caps.

Pictures from data

A template can take a picture per record from your data — a product photo on every catalogue card. The field it reads must hold one of:

  • the id of a PNG or JPEG picture uploaded to this workspace in the app (a key cannot upload pictures; the API generates documents only), or
  • a small inline picture: a data:image/png;base64,… or data:image/jpeg;base64,… value, at most 1 MB decoded.

Web addresses are never fetched — send the picture’s id instead. When a record’s value is empty or cannot be used, the document shows the template’s fallback picture if it has one, otherwise a “Picture unavailable” marker or, if the author chose it, empty space; the image-data-* warnings below say how many records that happened to. One document may use at most 200 different pictures from its data, 64 MB in total (422 image-limit-exceeded, never billed).

A chart from your own system. There is no chart block. A chart drawn by your own reporting tool or charting library goes in as a picture from data: save it as a PNG and send it inline in the field the picture reads, as above. It prints as the same picture in PDF, DOCX and HTML; its words are part of the picture, so they cannot be edited or searched.

Render warnings

Every code X-Ristmark-Render-Warnings can carry. In best-effort mode the document still returns. A counted code (<code>:N) gives the number of distinct problems — distinct blocks for pictures, distinct letterhead files, distinct bindings, lists or headers otherwise; the four picture-from-data codes (image-data-*, image-fallback-used) instead count the records they happened to — so the header stays short however much data you send, and it never contains your data. The last column says what strict: true does: refuses means 422 render_unresolved, whose diagnostics array lists each problem by its diagnostic code; never means it is a note — the document is what the template asks for — and a strict render returns it normally, with the note in the header.

Header code What happened Diagnostic code strict: true
unresolved-bindings:N A value the template reads was not in data, so that part may print blank. unresolved-path refuses
not-a-list:N A data-connected table, list or repeating section was given something that is not a list, so it prints no rows. not-a-list refuses
missing-columns:N A table has columns with no matching field in any data row, so those columns print empty. missing-columns refuses
money-unformatted:N A money amount could not be formatted — text that is not an amount, an unsupported currency code, or a currency symbol that is not the template’s currency ($1,234.50 in a EUR template) — so it printed exactly as sent (or blank, for a value that is not text). Send a plain number or an ISO-coded amount (EUR 1,234.50). money-unformatted refuses
invalid-date:N A date could not be read — a day that does not exist (2026-02-30), text that is not a date, a month with no day (October 2026), or a two-digit year (03/10/26) — so it printed exactly as sent. Send an ISO date (2026-10-03). invalid-date refuses
expression-error:N An expression or a show-when condition in the template could not be evaluated — a template problem, not a data one. eval-error, parse-error, unknown-function, condition-error refuses
block-overflow:N A block too tall for the page was not split and was placed whole, so it may run past the bottom of the page. block-overflow refuses
unknown-block-type:N The template holds a block of a kind this version cannot draw, so a labelled placeholder printed instead of its content. unknown-block-type refuses
qr-too-long:N A QR code’s value needs more bytes than one QR code holds at its error-correction level, so a labelled placeholder printed instead of the code. qr-too-long refuses
zone-overflow:N A fixed-height header or footer holds more than its height, so part of it prints over the page body. zone-overflow refuses
zone-measure-error:N A header or footer could not be measured, so it kept its stored height. zone-measure-error refuses
image-unresolved:N A picture could not be loaded — deleted, from another workspace, a web address (never fetched) or an undecodable data: value — so it is missing from the file. image-unresolved refuses
image-format-unsupported:N A picture is not a PNG or JPEG image, so it is missing from the file: one chosen by data (every format), or a GIF or WebP picture in the template (PDF and DOCX only — an HTML file includes it and carries no warning). image-format-unsupported refuses
image-data-not-found:N N records name a picture id that is not a PNG or JPEG picture uploaded to this workspace (missing, deleted, from another workspace, or not a picture), so the template’s fallback picture — or a “Picture unavailable” marker — printed instead. image-data-not-found refuses
image-data-invalid:N N records give a picture value that cannot name a picture: a web address (never fetched — upload the picture and send its id), an inline data: value that is not a base64 PNG or JPEG, or a value that is not text. The fallback or the marker printed instead. image-data-invalid refuses
letterhead-unresolved:N A letterhead PDF could not be loaded, so the pages that use it print without it. unresolved-resource refuses
image-data-empty:N N records have no picture value, so each shows what the template asks for then: its fallback picture, the “Picture unavailable” marker, or the empty space the author chose. image-data-empty never
image-fallback-used:N N records show the template’s fallback picture in place of their own. image-fallback-used never
empty-list:N A data-connected table, list or repeating section was given an empty list, so it prints no rows — a table keeps its header row. empty-list never
fixed-rows-shown:N A data-connected table found no data rows and printed its fixed rows instead. Reserved: a connected table now prints its header row only, so nothing produces this today. fixed-rows-shown never
zone-grew:N A flowing header or footer needed more room than its minimum height, so it grew and the page body starts lower. zone-grew never
zone-crowds-body:N A section’s header and footer leave less than 20 mm for the page body. zone-crowds-body never
anchored-over-content:N A block anchored to a fixed place on the page sits over something that body content which flows prints — words, a picture, a shape, or a table’s words, shading or rules, not just the space its block takes up — typically because your data made a table or text long enough to reach it. An anchored block keeps its place while the content flows, so whatever that content prints there is underneath it. N counts anchored blocks. anchored-over-content never
scan-code-too-fine:N A barcode’s narrowest bars, or a QR code’s squares, are thinner than one dot of a 300-dpi printer (0.085 mm), so such a printer cannot draw all of it. The code is drawn exactly as the template asks; whether a scanner reads it is not something a render can check. scan-code-too-fine never
word-broken:N A word is wider than the whole line it prints on — a long word in a large heading or a narrow column, or a long value in your data — so it is broken at the edge of its box and continues on the next line, in the PDF, the HTML and the DOCX alike. Every letter prints. N counts blocks. word-broken never
— A link whose address is unsafe (e.g. javascript:) keeps its text but loses its navigation, or a character a Code 39 barcode cannot encode was replaced by a space. Both are visible in the document itself, so neither has a header code. unsafe-link, barcode-charset refuses

Bare codes (no count) describe the render as a whole and do not refuse a strict render by themselves: language-fallback (the requested language was answered by its bare language), layout-error (an internal layout failure — the file is a single placeholder page, not your template, and is not billed; that page also carries an eval-error diagnostic, so a strict render of it is refused), sample-data-used (data was omitted or null), font-substituted, glyph-substituted, base-pdf-embed-failed (the letterhead could not be embedded into the PDF), letterhead-omitted-html and letterhead-omitted-docx (a letterhead is carried into PDF only), the DOCX simplifications list-markers-flattened, cell-paint-dropped, sheet-flattened (a ruled form — a sheet block — keeps its text but not its drawn layout: what its regions hold prints one under another, its lines as plain rules) and unrenderable-placeholder, and color-unreadable (a colour the template sets is not a CSS colour, so every output leaves that fill, highlight, border or line unpainted and prints text in it black). Five more describe writing systems an output cannot print correctly yet — measured in real renders, and warnings only (a render is never refused for its language): glyphs-missing (PDF: characters no embeddable font has, such as Chinese, Japanese, Korean, Hindi or Thai, print as empty boxes), rtl-word-order (PDF: Arabic or Hebrew phrases print with their words in the wrong order), rtl-paragraph-direction (HTML and DOCX: an Arabic or Hebrew paragraph is laid out left to right, so punctuation, digits or a Latin phrase at its edges land on the wrong side), cjk-text-overflow (HTML: a wrapping Chinese, Japanese or Korean paragraph is wider than its reserved space and runs into what follows) and japanese-letterforms (HTML: Japanese kanji may come from a Chinese font). See Languages and scripts for what each output does with each script.

Agent skill

Let your coding agent do the integration. The Ristmark generation skill (version 1.0.0, targets API v1) teaches an agent such as Claude Code or Codex the whole flow on this page: list templates, read a template’s data shape, map your data to it, generate one document and then a batch, and handle the errors. Download the skill (zip).

  1. Unzip it into your agent’s skills folder: for Claude Code, unzip ristmark-generation.zip -d ~/.claude/skills/ (or .claude/skills/ inside one project).
  2. For other agents, unzip anywhere and point the agent at ristmark-generation/SKILL.md; it is plain Markdown plus three example scripts.
  3. Put your key in the environment (export RISTMARK_API_KEY=...) and tell the agent what to integrate. The skill never needs the key pasted into a prompt, and it keeps the key out of source code and browsers.

Authoring skill (Beta)

A second skill, the Ristmark authoring skill (version 1.0.0, Beta, targets the Authoring API v1), teaches an agent to build a draft template for you: it reads the public template format and block catalogue, builds a template from your description or an existing document, creates a draft, repairs it from the API’s structured problem list, previews it with sample data, and gives you the editor link. It needs a separate authoring key (Settings → API keys → Build and edit draft templates (Beta)) and never generates documents. Drafts land in your review queue; a person approves them, and the agent never says a template is live. Text inside documents you give it is treated as content to copy, never as instructions. Download the authoring skill (zip).

  1. Unzip it the same way: unzip ristmark-authoring.zip -d ~/.claude/skills/ for Claude Code, or point another agent at ristmark-authoring/SKILL.md.
  2. Put your authoring key in the environment (export RISTMARK_API_KEY=...) and tell the agent what template to build.

The technical reference is served by the API at /v1/authoring/docs; the guide is Authoring API (Beta).