Skip to content

Template versions

A template has a name; a version has the content. Versions are numbered from 1, are never edited in place, and are never deleted. Publishing a change gives you name@N+1 and leaves name@N rendering byte-for-byte as it did.

That is the whole contract. Everything below follows from it.

ReferenceResolves to
invoiceThe latest version.
invoice@latestThe latest version.
invoice@The latest version.
invoice@3Version 3, forever.

Names are lowercased on the way in, so Invoice@3 and invoice@3 are the same reference. Anything else — invoice@v3, invoice@0, invoice@-1 — is a 400 invalid_request with the error on template:

{
"error": {
"type": "invalid_request",
"message": "Invalid template reference `invoice@v3`.",
"errors": [
{
"path": "template",
"message": "template must be `name` or `name@version`",
"expected": "string matching ^[a-z0-9][a-z0-9._-]{0,62}(@\\d+|@latest)?$",
"received": "invoice@v3",
"example": "invoice@3"
}
]
}
}

A render request can also carry the version separately, which the API folds into the ref for you:

{ "template": "invoice", "version": 3, "data": { } }
POST /v1/templates/:name/versions
curl -sS https://api.galleyrender.com/v1/templates/invoice/versions \
-H "Authorization: Bearer $GALLEY_API_KEY" \
-H 'content-type: application/json' \
-d '{
"source": "<!doctype html><html><head><style>@page{size:Letter;margin:0.5in}</style></head><body><h1>Invoice {{ invoice_number }}</h1><p>Remit to {{ seller.name }}</p></body></html>",
"engine": "chromium",
"schema": { "type": "object", "required": ["invoice_number", "seller"],
"properties": { "invoice_number": { "type": "string" },
"seller": { "type": "object", "required": ["name"],
"properties": { "name": { "type": "string" } } } } },
"options": { "page_size": "Letter", "margin": "0.5in", "print_background": true },
"example": { "invoice_number": "INV-1042", "seller": { "name": "Galley Render" } },
"message": "add remittance line"
}'
201 Created
{
"object": "template",
"id": "tpl_9f2c1d0a4b6e",
"name": "invoice",
"description": "Letter-size invoice with line items, totals, and payment terms.",
"latest_version": 2,
"version": 2,
"ref": "invoice@2",
"engine": "chromium",
"checksum": "5f1b…c74a",
"message": "add remittance line",
"version_created_at": "2026-09-16T14:02:11.004Z"
}

The body is the same shape as POST /v1/templates, minus name — the name comes from the URL. Every field is re-validated: the source must parse as Liquid, the schema must compile, the engine must be chromium or satori.

Publishing is free. There is no limit on how many versions a template accumulates.

GET /v1/templates/:name/versions
curl -sS https://api.galleyrender.com/v1/templates/invoice/versions \
-H "Authorization: Bearer $GALLEY_API_KEY"
{
"object": "list",
"data": [
{
"object": "template_version",
"id": "tv_4c81a0e2f7d3",
"template": "invoice",
"ref": "invoice@2",
"version": 2,
"engine": "chromium",
"checksum": "5f1b…c74a",
"message": "add remittance line",
"created_at": "2026-09-16T14:02:11.004Z"
},
{
"object": "template_version",
"id": "tv_1a09bb7c55e0",
"template": "invoice",
"ref": "invoice@1",
"version": 1,
"engine": "chromium",
"checksum": "0b7e…9a12",
"message": "starter library",
"created_at": "2026-09-16T09:41:55.870Z"
}
]
}

Newest first. The listing is metadata only — no source, schema or options. To read a version in full, GET /v1/templates/invoice@1, which returns source, schema, options, example and checksum alongside the fields above.

Each version carries a checksum: a SHA-256 over exactly four inputs, in a canonical form.

In the checksumNot in the checksum
enginename
source (the raw string)version number
schema (canonical JSON)description
options (canonical JSON)example
message
timestamps and ids

Canonical JSON means key order and whitespace do not matter. {"margin":"18mm","page_size":"A4"} and {"page_size":"A4","margin":"18mm"} produce the same checksum.

Two consequences worth knowing:

  • Cosmetic changes are free. Publish a new version that only fixes the message, the description or the example and the checksum is unchanged.
  • Whitespace is not cosmetic. The source is hashed as a raw string, so re-indenting the HTML changes the checksum even though the rendered document is identical.

The render cache key is a SHA-256 over four things:

template version checksum
output format
canonical JSON of data
canonical JSON of merged options

The version’s checksum is what identifies the template there, not name@N. So:

You publishChecksumEffect on the cache
A real change to source, schema, options or engineNewThe new version starts cold. Every render against it is a fresh render and a fresh charge.
A change to message, description or example onlySameRenders against the new version hit entries cached under the old one. Free and instant.
The exact source, schema, options and engine of an older versionSame as that older versionThe new version inherits that version’s warm cache.

That last row is the mechanism behind rollback.

Two ways, depending on what you control.

Pin the good version. If your code sends invoice@3, a bad invoice@4 never reaches production. Nothing to undo — stop pinning latest and the problem does not exist.

Re-publish the good version. If callers you do not control are on latest, read the version you want back and publish it again:

Terminal window
# 1. Pull version 3 in full.
curl -sS https://api.galleyrender.com/v1/templates/invoice@3 \
-H "Authorization: Bearer $GALLEY_API_KEY" > v3.json
# 2. Re-publish its source, schema, options and engine as version 5.
jq '{source, schema, options, engine, example, message: "roll back to v3"}' v3.json \
| curl -sS https://api.galleyrender.com/v1/templates/invoice/versions \
-H "Authorization: Bearer $GALLEY_API_KEY" \
-H 'content-type: application/json' --data-binary @-

Version 5 has the same checksum as version 3, which means it also inherits version 3’s cached renders: a rollback is instant and costs nothing for documents that were already produced.

There is no “delete version 4”. History is append-only, and version 4 keeps rendering for anyone who pinned it.

DELETE /v1/templates/:name
curl -sS -X DELETE https://api.galleyrender.com/v1/templates/delivery-note \
-H "Authorization: Bearer $GALLEY_API_KEY"
{ "object": "template", "id": "tpl_2b77e10c93aa", "name": "delivery-note", "deleted": true }

This is a soft delete. The template is marked deleted and:

Still trueNo longer true
Existing renders keep their stored files; GET /v1/renders/:id still re-signs a URL.The template disappears from GET /v1/templates.
The version rows and their checksums are kept.POST /v1/render with that template returns 404 not_found.
The name stays reserved on the account.Nothing resolves the name any more, including name@1.

The MCP tools map one to one:

ToolEndpoint
create_templatePOST /v1/templates
update_templatePOST /v1/templates/:name/versions
get_templateGET /v1/templates/:ref
list_templatesGET /v1/templates

update_template takes the template name, never name@N — you cannot publish “a new version of version 2”. See The MCP server.