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.
Referencing a version
Section titled “Referencing a version”| Reference | Resolves to |
|---|---|
invoice | The latest version. |
invoice@latest | The latest version. |
invoice@ | The latest version. |
invoice@3 | Version 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": { } }Publishing a new version
Section titled “Publishing a new version”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" }'{ "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.
Listing versions
Section titled “Listing 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.
The checksum
Section titled “The checksum”Each version carries a checksum: a SHA-256 over exactly four inputs, in a canonical form.
| In the checksum | Not in the checksum |
|---|---|
engine | name |
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, thedescriptionor theexampleand 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.
Versions and the render cache
Section titled “Versions and the render cache”The render cache key is a SHA-256 over four things:
template version checksumoutput formatcanonical JSON of datacanonical JSON of merged optionsThe version’s checksum is what identifies the template there, not name@N. So:
| You publish | Checksum | Effect on the cache |
|---|---|---|
| A real change to source, schema, options or engine | New | The new version starts cold. Every render against it is a fresh render and a fresh charge. |
A change to message, description or example only | Same | Renders against the new version hit entries cached under the old one. Free and instant. |
| The exact source, schema, options and engine of an older version | Same as that older version | The new version inherits that version’s warm cache. |
That last row is the mechanism behind rollback.
Rolling back
Section titled “Rolling back”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:
# 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.
Deleting a template
Section titled “Deleting a template”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 true | No 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. |
From an agent
Section titled “From an agent”The MCP tools map one to one:
| Tool | Endpoint |
|---|---|
create_template | POST /v1/templates |
update_template | POST /v1/templates/:name/versions |
get_template | GET /v1/templates/:ref |
list_templates | GET /v1/templates |
update_template takes the template name, never name@N — you cannot publish “a new version
of version 2”. See The MCP server.