Skip to content
CastPDF
API reference

Templates and fields

Create, list, update, version, pin, preview and delete PDF templates, and read a template's fields for forms in no-code tools.

On this page

A template stores HTML with Liquid placeholders, optional CSS, sample data and page settings. Every content change creates a new version, and a pinned version keeps your live PDFs stable while you edit. The concepts are explained in Templates and versions; this page lists the endpoints. Most teams create templates in the dashboard and only render them through the API, but everything the dashboard does with a template is available here too.

The template object

Create, get and update return the full template object:

Template
{
  "id": "8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b",
  "name": "Invoice",
  "current_version": 4,
  "pinned_version": 3,
  "updated_at": "2026-09-30T09:12:44.518Z",
  "thumbnail_url": "https://api.castpdf.com/v1/thumbnails/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b.png?v=4&exp=1790935200&sig=Lw8d...",
  "version": 4,
  "html": "<h1>Invoice {{ number }}</h1>...",
  "css": "h1 { color: #4338ca; }",
  "sample_data": { "number": "INV-001", "total": 99.5 },
  "settings": { "format": "A4", "margin": "20mm 15mm" }
}
FieldDescription
idstring (UUID)The template's id, for template_id on POST /v1/pdf.
namestringThe name, also the default file name of its PDFs.
current_versionintegerThe newest version.
pinned_versioninteger or nullThe version renders use when they name none; null means the current one.
updated_atstringThe last change, of any kind.
thumbnail_urlstringA signed PNG of the current version's first page, rendered with its sample data.
versionintegerThe version whose content follows.
html, cssstring, string or nullThe Liquid template and its stylesheet.
sample_dataobject or nullThe data used when a render sends none, and for previews and thumbnails.
settingsobjectThe page defaults: mode, format, orientation and margin.

The list and pin endpoints return the summary: the fields down to thumbnail_url, without the content.

Test keys can read and preview templates, but not change them: create, update, pin and delete need a live key (or the dashboard), and a test key gets 403 forbidden. A leaked test key then cannot alter the templates your live documents use.

Templates per team: Free 5, Starter 25, Growth 100, Pro 2,000 and Scale 2,000 ("Unlimited" on the pricing page is this fair-use limit). Deleted templates do not count toward that number, but they are kept for 30 days, and all templates together, deleted ones included, are limited to that number plus 100. Template changes (create, update, pin and delete together) are limited to 60 a minute per team, shared with the dashboard.

POST /v1/templates

Creates a template with version 1.

Request
curl https://api.castpdf.com/v1/templates \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Invoice",
    "html": "<h1>Invoice {{ number }}</h1><p>Total: {{ total | money: \"USD\" }}</p>",
    "css": "h1 { color: #4338ca; }",
    "sample_data": {"number": "INV-001", "total": 99.5},
    "settings": {"format": "A4", "margin": "20mm 15mm"}
  }'
const res = await fetch('https://api.castpdf.com/v1/templates', {
  method: 'POST',
  headers: { Authorization: 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
  body: JSON.stringify({
    name: 'Invoice',
    html: '<h1>Invoice {{ number }}</h1><p>Total: {{ total | money: "USD" }}</p>',
    css: 'h1 { color: #4338ca; }',
    sample_data: { number: 'INV-001', total: 99.5 },
    settings: { format: 'A4', margin: '20mm 15mm' },
  }),
});
const template = await res.json();
console.log(template.id, template.current_version); // ..., 1
import requests

res = requests.post(
    "https://api.castpdf.com/v1/templates",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={
        "name": "Invoice",
        "html": '<h1>Invoice {{ number }}</h1><p>Total: {{ total | money: "USD" }}</p>',
        "css": "h1 { color: #4338ca; }",
        "sample_data": {"number": "INV-001", "total": 99.5},
        "settings": {"format": "A4", "margin": "20mm 15mm"},
    },
    timeout=30,
)
template = res.json()
print(template["id"], template["current_version"])  # ..., 1
Response
{
  "id": "8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b",
  "name": "Invoice",
  "current_version": 1,
  "pinned_version": null,
  "updated_at": "2026-10-11T09:12:44.518Z",
  "thumbnail_url": "https://api.castpdf.com/v1/thumbnails/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b.png?v=1&exp=1791716400&sig=Lw8d...",
  "version": 1,
  "html": "<h1>Invoice {{ number }}</h1><p>Total: {{ total | money: \"USD\" }}</p>",
  "css": "h1 { color: #4338ca; }",
  "sample_data": { "number": "INV-001", "total": 99.5 },
  "settings": { "format": "A4", "margin": "20mm 15mm" }
}
{
  "error": {
    "code": "plan_limit_reached",
    "message": "Your plan allows 5 templates. Delete one, or upgrade.",
    "docs_url": "https://castpdf.com/docs/errors#plan_limit_reached",
    "details": { "limit": "templates", "max": 5 }
  }
}
FieldDescription
namestring, 1 to 100 charactersRequired
htmlstring, not emptyRequiredThe Liquid template, at most 500 KB. It is parsed on save: a syntax error or an unknown filter is rejected with template_render_error and its line and column.
cssstringAt most 500 KB.
sample_dataobjectDefault data for renders without data, and for previews. At most 1 MB as JSON.
settingsobjectPage defaults: mode, format, orientation and margin (singular), with the same values as the POST /v1/pdf fields.
CodeWhen
invalid_request (400)Validation failed, or css or sample_data is too large
plan_limit_reached (402)The plan's template limit (details: { "limit": "templates", "max": N }), or the limit on all templates including recently deleted ones ("limit": "templates_including_deleted")
forbidden (403)A test key
template_render_error (422)The HTML does not parse as Liquid
rate_limited (429)Over the template change limit or the key's rate limit

GET /v1/templates

Lists templates, most recently updated first, without their content.

Request
curl "https://api.castpdf.com/v1/templates?limit=20&offset=0" \
  -H "Authorization: Bearer YOUR_API_KEY"
const res = await fetch('https://api.castpdf.com/v1/templates?limit=20', {
  headers: { Authorization: 'Bearer YOUR_API_KEY' },
});
const { data, has_more } = await res.json();
import requests

res = requests.get(
    "https://api.castpdf.com/v1/templates",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    params={"limit": 20},
    timeout=30,
)
page = res.json()
200
{
  "data": [
    {
      "id": "8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b",
      "name": "Invoice",
      "current_version": 4,
      "pinned_version": 3,
      "updated_at": "2026-09-30T09:12:44.518Z",
      "thumbnail_url": "https://api.castpdf.com/v1/thumbnails/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b.png?v=4&exp=1790935200&sig=Lw8d..."
    }
  ],
  "has_more": false
}
Query parameterDescription
limit1 to 100. Default 50.
offsetHow many to skip. Default 0.

has_more is true when another page follows: ask again with offset increased by limit.

GET /v1/templates/:id

Returns the full template object with the current version's content, or with version n when you add ?version=n.

curl
curl "https://api.castpdf.com/v1/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b?version=3" \
  -H "Authorization: Bearer YOUR_API_KEY"
Query parameterDescription
versionA version number. Default: the current version (not the pinned one).
CodeWhen
template_not_found (404)An unknown or deleted template, or a version that does not exist (any more)

PUT /v1/templates/:id

Updates a template. Send at least one field; the fields are those of POST /v1/templates, all optional.

  • Any change to html, css, sample_data or settings creates a new version with the next number; fields you leave out are copied from the current version. settings replaces the stored settings object as a whole. css can be set to null to remove it.
  • A change of name alone creates no version.
  • The latest 50 versions are kept, plus the pinned one; older ones are deleted.
Request
curl -X PUT https://api.castpdf.com/v1/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"css": "h1 { color: #0f172a; }"}'
const res = await fetch('https://api.castpdf.com/v1/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b', {
  method: 'PUT',
  headers: { Authorization: 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
  body: JSON.stringify({ css: 'h1 { color: #0f172a; }' }),
});
const template = await res.json();
console.log(template.current_version); // the new version number
import requests

res = requests.put(
    "https://api.castpdf.com/v1/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={"css": "h1 { color: #0f172a; }"},
    timeout=30,
)
print(res.json()["current_version"])

200 with the full template object at its (new) current version.

CodeWhen
invalid_request (400)Validation failed, an empty body, or css or sample_data is too large
forbidden (403)A test key
template_not_found (404)An unknown or deleted template
template_render_error (422)The new HTML does not parse as Liquid

DELETE /v1/templates/:id

Deletes a template. Later renders with its id fail with template_not_found; documents already generated are not affected. The template and all its versions are erased for good 30 days later.

Request
curl -X DELETE https://api.castpdf.com/v1/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b \
  -H "Authorization: Bearer YOUR_API_KEY"
const res = await fetch('https://api.castpdf.com/v1/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b', {
  method: 'DELETE',
  headers: { Authorization: 'Bearer YOUR_API_KEY' },
});
console.log(res.status); // 204
import requests

res = requests.delete(
    "https://api.castpdf.com/v1/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    timeout=30,
)
print(res.status_code)  # 204

204 with no body.

CodeWhen
forbidden (403)A test key
template_not_found (404)An unknown or already deleted template

GET /v1/templates/:id/fields

The fields a template expects, read from the sample data of its current version (or of ?version=n). No-code tools use it to show a form with one input per field, so the person building the automation does not have to know the template's data by heart.

curl
curl https://api.castpdf.com/v1/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b/fields \
  -H "Authorization: Bearer YOUR_API_KEY"
200
{
  "template_id": "8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b",
  "version": 4,
  "fields": [
    { "key": "number", "type": "string", "sample": "INV-001" },
    { "key": "paid", "type": "boolean", "sample": false },
    { "key": "customer.name", "type": "string", "sample": "Acme Ltd" }
  ],
  "line_items": [
    {
      "key": "items",
      "fields": [
        { "key": "description", "type": "string", "sample": "Design" },
        { "key": "quantity", "type": "number", "sample": 2 },
        { "key": "price", "type": "number", "sample": 50 }
      ],
      "sample_count": 1
    }
  ],
  "tree": [
    { "name": "number", "label": "number", "type": "text", "help": "Fills `number` in the template. Example: INV-001" },
    { "name": "paid", "label": "paid", "type": "boolean", "help": "Fills `paid` in the template." },
    {
      "name": "customer",
      "label": "customer",
      "type": "collection",
      "help": "The fields of `customer` in the template.",
      "spec": [
        { "name": "name", "label": "name", "type": "text", "help": "Fills `customer.name` in the template. Example: Acme Ltd" }
      ]
    },
    {
      "name": "items",
      "label": "items",
      "type": "array",
      "help": "Items of `items` in the template, one per row (for example the line items of an order). Map an array, or add the items one by one.",
      "spec": [
        { "name": "description", "label": "description", "type": "text", "help": "Fills `description` in each item of `items`. Example: Design" },
        { "name": "quantity", "label": "quantity", "type": "number", "help": "Fills `quantity` in each item of `items`. Example: 2" },
        { "name": "price", "label": "price", "type": "number", "help": "Fills `price` in each item of `items`. Example: 50" }
      ]
    }
  ],
  "tree_truncated": false
}

This is the real answer for a version whose sample data is { "number": "INV-001", "paid": false, "customer": { "name": "Acme Ltd" }, "items": [{ "description": "Design", "quantity": 2, "price": 50 }] }. The response describes the same fields twice, for two kinds of tools:

FieldDescription
fieldsarrayEvery plain value by its dotted path (customer.name), with type (string, number, boolean, or list for a list of plain values) and the sample value.
line_itemsarrayEvery list of objects (such as the rows of an invoice): its key, the fields of one item (keys relative to the item, merged across the first items of the sample so optional keys are found too) and sample_count, the number of items in the sample.
treearrayThe same fields as a ready-made nested form. name is the data key exactly as in the sample data, spaces and dots included, never a path. type is text, number, boolean, collection (an object; spec holds its fields) or array (spec holds the fields of one item, or one value field for a list of plain values). help says what the field fills.
tree_truncatedbooleantrue when fields were left out of tree: more than 200 in all, deeper than 6 levels, or lists that mix values and objects.

fields and line_items hold up to 200 fields in all, 6 levels deep, and skip keys with characters other than letters, digits, _, $ and -. A template without sample data has no fields: every list is empty.

Sending what a form produced. A form built from tree produces nested objects in exactly the shape of your data, often with empty values for every field nobody filled. Send that as merge_data on POST /v1/pdf: empty values (null, "", empty objects and lists) are dropped first, so they never overwrite anything, and the rest is merged deeply over data (objects key by key; a list replaces the list it meets). Without data, the non-empty values alone become the data, and the sample data is used only when nothing is left.

CodeWhen
template_not_found (404)An unknown or deleted template, or a version that does not exist

GET /v1/templates/:id/versions

Lists the versions that still exist, newest first.

curl
curl https://api.castpdf.com/v1/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b/versions \
  -H "Authorization: Bearer YOUR_API_KEY"
200
{
  "data": [
    { "version": 4, "created_at": "2026-09-30T09:12:44.518Z" },
    { "version": 3, "created_at": "2026-09-29T16:02:10.004Z" }
  ]
}

The list is not paginated: at most the latest 50 versions plus the pinned one exist. Errors: template_not_found (404).

POST /v1/templates/:id/pin

Pins a version: renders with this template_id and no template_version then use it, even after later updates. Send null to unpin, so renders follow the current version again.

curl
curl https://api.castpdf.com/v1/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b/pin \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"version": 3}'
200
{
  "id": "8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b",
  "name": "Invoice",
  "current_version": 4,
  "pinned_version": 3,
  "updated_at": "2026-10-11T09:20:01.007Z",
  "thumbnail_url": "https://api.castpdf.com/v1/thumbnails/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b.png?v=4&exp=1791716400&sig=Lw8d..."
}
FieldDescription
versioninteger, 1 or more, or nullRequired

200 with the template summary, without content.

CodeWhen
forbidden (403)A test key
template_not_found (404)An unknown template, or a version that does not exist

POST /v1/templates/:id/preview

Renders a template as a watermarked PDF, for checking a template or a version before you use it. Previews are never counted, never stored and do not appear in the document log, with any key.

Request
curl https://api.castpdf.com/v1/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b/preview \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"version": 4, "data": {"number": "INV-002", "total": 250}}' \
  --output preview.pdf
import { writeFile } from 'node:fs/promises';

const res = await fetch('https://api.castpdf.com/v1/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b/preview', {
  method: 'POST',
  headers: { Authorization: 'Bearer YOUR_API_KEY', 'Content-Type': 'application/json' },
  body: JSON.stringify({ version: 4, data: { number: 'INV-002', total: 250 } }),
});
console.log(res.headers.get('x-warnings'));
await writeFile('preview.pdf', Buffer.from(await res.arrayBuffer()));
import requests

res = requests.post(
    "https://api.castpdf.com/v1/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b/preview",
    headers={"Authorization": "Bearer YOUR_API_KEY"},
    json={"version": 4, "data": {"number": "INV-002", "total": 250}},
    timeout=60,
)
res.raise_for_status()
with open("preview.pdf", "wb") as f:
    f.write(res.content)
FieldDescription
dataobjectThe Liquid data. Default: the version's sample_data.
versioninteger, 1 or moreDefault: the pinned version, or else the current one.

The template's page settings apply; the page options of POST /v1/pdf are not accepted here. The answer is 200 with content-type: application/pdf, content-disposition: inline; filename="Invoice.pdf", cache-control: no-store and x-pages, plus x-warnings (see warnings). Previews have their own limit of 30 per minute per team and key mode, on top of the key's rate limit. Errors are as for POST /v1/pdf, without the plan limits.

Updated Something unclear? [email protected], a real person replies.