Skip to content

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

GET /api/v1/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

GET /api/v1/templates/{template_id}

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.

POST /api/v1/templates/{template_id}/preview

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

GET /api/v1/templates/builtin/{template_id}

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

POST /api/v1/templates/custom

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

Response201 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

GET /api/v1/templates/custom

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

GET /api/v1/templates/custom/{template_id}

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

PATCH /api/v1/templates/custom/{template_id}

Change the name, description, or config. All fields are optional; omitted fields are left unchanged. Returns the updated template.

Request:

{
  "config": { "primary_color": "#111827", "footer_text": "Thank you!" }
}
Try it

Duplicate a Custom Template

POST /api/v1/templates/custom/{template_id}/duplicate

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

POST /api/v1/templates/custom/{template_id}/publish

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

DELETE /api/v1/templates/custom/{template_id}

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

GET /api/v1/templates/{template_id}/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

POST /api/v1/templates/{template_id}/versions

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:

{
  "label": "Brighter blue",
  "changelog": "Lightened the accent"
}
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

GET /api/v1/templates/{template_id}/versions/{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

POST /api/v1/templates/{template_id}/versions/{version}/restore

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.