Templates API¶
List available invoice templates and preview them with sample data.
Available templates¶
Six built-in styles ship out of the box. Preview any of them with your own data
via the preview endpoint, or select one at render time with
rendering.template_id on the Documents render endpoint.
template_id |
Name | Style |
|---|---|---|
tpl_modern |
Modern | Clean and contemporary — tracked wordmark, a TOTAL DUE callout, generous spacing |
tpl_classic |
Classic | Formal serif with a double-rule header and a filled header band |
tpl_minimal |
Minimal | Monochrome and airy — hairline rules, lots of whitespace, no fills |
tpl_compact |
Compact | Dense monospace that fits more line items per page, with boxed totals |
tpl_bold |
Bold | High-impact — filled accent header band and a solid TOTAL DUE block |
tpl_refined |
Refined | Display heading and a full-width total band (adapted from the WeasyPrint sample) |
All templates are accent-aware: set branding.primary_color / accent_color
(see the Branding API) and the template recolours to match; otherwise
each keeps its own signature colour.
Beyond the built-ins you can create custom templates derived from a base style
via the custom-template endpoints (/api/v1/templates/custom — create, list, publish,
delete). A custom template is a built-in plus a brand: it names one of the six in
base_template_id and carries the colours, font and text to render it in.
Its ctpl_* id goes anywhere a tpl_* id does — POST /documents/render,
POST /documents/{id}/renders, POST /batches, and this page's preview endpoint —
and the template's config is applied underneath the document's own
data.branding, so a document that states a colour still wins and one template can
serve several brands. Drafts render as readily as published templates.
List Templates¶
Response:
{
"data": [
{ "template_id": "tpl_modern", "name": "Modern", "document_type": "invoice" },
{ "template_id": "tpl_classic", "name": "Classic", "document_type": "invoice" },
{ "template_id": "tpl_minimal", "name": "Minimal", "document_type": "invoice" },
{ "template_id": "tpl_bold", "name": "Bold", "document_type": "invoice" },
{ "template_id": "tpl_compact", "name": "Compact", "document_type": "invoice" },
{ "template_id": "tpl_refined", "name": "Refined", "document_type": "invoice" }
]
}
Try it
Get Template Details¶
Response:
{
"data": {
"template_id": "tpl_modern",
"name": "Modern",
"document_type": "invoice",
"engine": "html-css"
}
}
| Field | Description |
|---|---|
template_id |
Unique template identifier |
name |
Human-readable template name |
document_type |
Document type this template supports (currently invoice) |
engine |
Rendering engine used (html-css) |
Try it
Preview Template¶
Render a PDF preview using a specific template. The request body matches the Documents render endpoint, allowing you to test different templates with the same data.
Request:
{
"document_type": "invoice",
"data": {
"invoice_number": "PREVIEW-001",
"issue_date": "2026-07-20",
"due_date": "2026-08-19",
"currency": "USD",
"seller": {
"name": "Your Company",
"email": "billing@yourcompany.com",
"address": {
"line1": "1 Main St",
"city": "San Francisco",
"state": "CA",
"postal_code": "94105",
"country": "US"
}
},
"buyer": {
"name": "Sample Customer",
"email": "customer@example.com"
},
"line_items": [
{
"name": "Sample Service",
"quantity": "1",
"unit_price": "99.00",
"taxes": [{ "name": "Tax", "rate": "10", "inclusive": false }]
}
]
},
"output": { "format": "pdf", "delivery": "url" }
}
Response:
{
"data": {
"id": "rnd_01ABC",
"status": "completed",
"format": "pdf",
"download_url": "/api/v1/renders/rnd_01ABC/download",
"calculation": {
"subtotal": { "amount": "99.00", "currency": "USD" },
"discount_total": { "amount": "0.00", "currency": "USD" },
"tax_total": { "amount": "9.90", "currency": "USD" },
"shipping_total": { "amount": "0.00", "currency": "USD" },
"total": { "amount": "108.90", "currency": "USD" }
},
"expires_at": "2026-07-20T01:00:00Z",
"created_at": "2026-07-20T00:00:00Z"
}
}
Binary PDF response
Set Accept: application/pdf header or "delivery": "binary" in the output options to get the PDF bytes directly instead of a download URL.
Try it
Get a Built-in Template¶
Fetch a single built-in style by id (tpl_modern, tpl_classic, tpl_minimal,
tpl_compact, tpl_bold, tpl_refined). Returns 404 not_found for an unknown id.
Response:
{
"data": {
"template_id": "tpl_modern",
"name": "Modern",
"document_type": "invoice",
"engine": "html-css"
}
}
Try it
Custom Templates¶
Custom templates let you derive your own named style from a built-in base and
adjust its config. A template starts as a draft, can be revised, and
becomes selectable at render time once published. Reference one when
rendering with rendering.template_id (see the Documents render endpoint).
Create a Custom Template¶
Request:
{
"name": "Acme Branded",
"description": "Company invoice style with brand colors",
"base_template_id": "tpl_modern",
"config": {
"primary_color": "#0066CC",
"accent_color": "#003366",
"font_family": "Inter"
}
}
| Field | Required | Description |
|---|---|---|
name |
Yes | Display name for the template |
base_template_id |
No | Built-in style to derive from (default tpl_modern; must be a valid tpl_* id, else 422 invalid_base_template) |
description |
No | Free-text note |
config |
No | The brand to render the base in — see below. Unknown keys are rejected (422) |
config accepts exactly these, all optional; an omitted one means the template has
no opinion on it and the document decides:
| Key | Description |
|---|---|
primary_color |
Hex colour, e.g. #0066CC |
accent_color |
Hex colour |
font_family |
Font stack, e.g. Inter |
header_text |
Text printed in the document header |
footer_text |
Text printed in the page footer |
Response — 201 Created, status draft:
{
"data": {
"id": "ctpl_01ABC",
"name": "Acme Branded",
"description": "Company invoice style with brand colors",
"base_template_id": "tpl_modern",
"config": { "primary_color": "#0066CC", "accent_color": "#003366", "font_family": "Inter" },
"status": "draft",
"is_default": false,
"created_at": "2026-08-06T09:00:00Z",
"updated_at": "2026-08-06T09:00:00Z",
"published_at": null
}
}
Try it
List Custom Templates¶
Cursor-paginated list of your custom templates. Query params: limit (1–100,
default 50) and cursor.
Response:
{
"data": [
{
"id": "ctpl_01ABC",
"name": "Acme Branded",
"base_template_id": "tpl_modern",
"status": "published",
"is_default": true,
"created_at": "2026-08-06T09:00:00Z",
"updated_at": "2026-08-06T09:10:00Z",
"published_at": "2026-08-06T09:10:00Z"
}
],
"pagination": { "has_more": false, "next_cursor": null }
}
Try it
Get a Custom Template¶
Fetch one custom template by its ctpl_* id, including its full config.
Response:
{
"data": {
"id": "ctpl_01ABC",
"name": "Acme Branded",
"description": "Company invoice style with brand colors",
"base_template_id": "tpl_modern",
"config": { "primary_color": "#0066CC", "accent_color": "#003366", "font_family": "Inter" },
"status": "published",
"is_default": true,
"created_at": "2026-08-06T09:00:00Z",
"updated_at": "2026-08-06T09:10:00Z",
"published_at": "2026-08-06T09:10:00Z"
}
}
Try it
Update a Custom Template¶
Change the name, description, or config. All fields are optional; omitted
fields are left unchanged. Returns the updated template.
Request:
Try it
Duplicate a Custom Template¶
Create an independent copy — useful for iterating on a published template
without touching the live one. The copy starts as a fresh draft. Returns
201 Created with the new template.
Try it
Publish a Custom Template¶
Move a draft to published so it can be selected at render time. Publishing an
already-published template returns 409 conflict.
Response — status flips to published with a published_at timestamp:
{
"data": {
"id": "ctpl_01ABC",
"name": "Acme Branded",
"status": "published",
"published_at": "2026-08-06T09:10:00Z"
}
}
Try it
Delete a Custom Template¶
Permanently remove a custom template. Documents already rendered are unaffected.
Returns 204 No Content.
Try it
Template Versions¶
Versions are point-in-time snapshots of a custom template's config, each with
a label and changelog, so you can track how a template evolved and put it
back. A version records what the template held at the moment you took it —
you cannot supply a config, and there is nothing to version on a built-in
(tpl_*) id, which has no config and cannot be changed.
List Template Versions¶
Return every saved version of a template, newest version first.
Response:
{
"data": [
{ "id": "tv_02DEF", "template_id": "ctpl_01ABC", "version": 2, "label": "Brighter blue", "changelog": "Lightened the accent", "config": { "primary_color": "#2563EB", "accent_color": "#2563EB" }, "created_at": "2026-08-06T10:00:00Z" },
{ "id": "tv_01ABC", "template_id": "ctpl_01ABC", "version": 1, "label": "Initial release", "changelog": "First version", "config": { "primary_color": "#0066CC" }, "created_at": "2026-08-06T09:10:00Z" }
]
}
Try it
Create a Template Version¶
Snapshot the template's current config as a new, auto-incremented version
(v1, v2, …). The config is read from the template — the request carries only the
words describing the change.
Request:
| Field | Required | Description |
|---|---|---|
label |
No | Short name for the version |
changelog |
No | What changed in this version |
Response:
{
"data": {
"id": "tv_03GHI",
"template_id": "ctpl_01ABC",
"version": 3,
"label": "Brighter blue",
"changelog": "Lightened the accent",
"config": { "primary_color": "#2563EB", "accent_color": "#2563EB" },
"created_at": "2026-08-06T11:00:00Z"
}
}
Try it
Get a Template Version¶
Fetch a single version by its integer version number.
Response:
{
"data": {
"id": "tv_02DEF",
"template_id": "ctpl_01ABC",
"version": 2,
"label": "Brighter blue",
"changelog": "Lightened the accent",
"config": { "primary_color": "#2563EB", "accent_color": "#2563EB" },
"created_at": "2026-08-06T10:00:00Z"
}
}
Try it
Restore a Template Version¶
Put the template back to the config this version recorded. The next render uses it immediately.
The template moves; the history does not. Restoring v1 over v3's config does not delete v3 or renumber anything — the next snapshot is v4 — so take a version first if the config being replaced is worth keeping.
Restoring is the right move when you want the template itself back at an earlier config. To render one document as it looked then, without touching what everything else renders from, pin the version on the render instead — see Rendering a pinned version.
Response — the version that was restored:
{
"data": {
"id": "tv_01ABC",
"template_id": "ctpl_01ABC",
"version": 1,
"label": "Initial release",
"changelog": "First version",
"config": { "primary_color": "#0066CC" },
"created_at": "2026-08-06T09:10:00Z"
}
}
Try it
Rendering a pinned version¶
A template's config changes; a document you already sent should not. Pin a version on the render and it uses the config that version recorded, while the template goes on being edited for everything else.
Every render path takes one:
| Endpoint | How |
|---|---|
POST /api/v1/documents/render |
"template": { "id": "ctpl_01ABC", "version": 2 } |
POST /api/v1/documents/{id}/renders |
"template_id": "ctpl_01ABC", "template_version": 2 |
POST /api/v1/batches |
"template_id": "ctpl_01ABC", "template_version": 2 |
POST /api/v1/templates/{template_id}/preview |
?version=2 |
Omit it and the render uses whatever the template holds — which for a batch means whatever it holds when each item runs, not when the batch was submitted.
The render records what it was made from, and still says so long after the template has moved on:
{
"data": {
"id": "rnd_01ABC",
"template_id": "ctpl_01ABC",
"template_version": 2,
"status": "completed",
"format": "pdf",
"download_url": "/api/v1/renders/rnd_01ABC/download"
}
}
A version that cannot be honoured is refused rather than quietly dropped:
pinning a version on a built-in (tpl_*) id is 422 — built-ins have no
versions — and a version number the template has never had is 404.