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:
{
"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" }
}| Field | Description |
|---|---|
idstring (UUID) | The template's id, for template_id on POST /v1/pdf. |
namestring | The name, also the default file name of its PDFs. |
current_versioninteger | The newest version. |
pinned_versioninteger or null | The version renders use when they name none; null means the current one. |
updated_atstring | The last change, of any kind. |
thumbnail_urlstring | A signed PNG of the current version's first page, rendered with its sample data. |
versioninteger | The version whose content follows. |
html, cssstring, string or null | The Liquid template and its stylesheet. |
sample_dataobject or null | The data used when a render sends none, and for previews and thumbnails. |
settingsobject | The 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.
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); // ..., 1import 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{
"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 }
}
}| Field | Description |
|---|---|
namestring, 1 to 100 charactersRequired | |
htmlstring, not emptyRequired | The 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. |
cssstring | At most 500 KB. |
sample_dataobject | Default data for renders without data, and for previews. At most 1 MB as JSON. |
settingsobject | Page defaults: mode, format, orientation and margin (singular), with the same values as the POST /v1/pdf fields. |
| Code | When |
|---|---|
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.
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(){
"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 parameter | Description |
|---|---|
limit | 1 to 100. Default 50. |
offset | How 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 "https://api.castpdf.com/v1/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b?version=3" \
-H "Authorization: Bearer YOUR_API_KEY"| Query parameter | Description |
|---|---|
version | A version number. Default: the current version (not the pinned one). |
| Code | When |
|---|---|
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_dataorsettingscreates a new version with the next number; fields you leave out are copied from the current version.settingsreplaces the stored settings object as a whole.csscan be set tonullto remove it. - A change of
namealone creates no version. - The latest 50 versions are kept, plus the pinned one; older ones are deleted.
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 numberimport 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.
| Code | When |
|---|---|
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.
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); // 204import 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) # 204204 with no body.
| Code | When |
|---|---|
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 https://api.castpdf.com/v1/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b/fields \
-H "Authorization: Bearer YOUR_API_KEY"{
"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:
| Field | Description |
|---|---|
fieldsarray | Every 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_itemsarray | Every 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. |
treearray | The 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_truncatedboolean | true 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.
| Code | When |
|---|---|
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 https://api.castpdf.com/v1/templates/8d1f2c3b-4a5e-4f60-9b7a-1c2d3e4f5a6b/versions \
-H "Authorization: Bearer YOUR_API_KEY"{
"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 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}'{
"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..."
}| Field | Description |
|---|---|
versioninteger, 1 or more, or nullRequired |
200 with the template summary, without content.
| Code | When |
|---|---|
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.
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.pdfimport { 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)| Field | Description |
|---|---|
dataobject | The Liquid data. Default: the version's sample_data. |
versioninteger, 1 or more | Default: 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.