POST /v1/templates
Creates a new template and publishes it as version 1. Free — creating templates costs no renders.
POST https://api.galleyrender.com/v1/templatesAuthorization: Bearer glr_sk_…Content-Type: application/jsonUse this when nothing in the starter library fits. Changing an existing template is
POST /v1/templates/:name/versions, not this endpoint — a name that is
already taken returns 409 conflict.
Request
Section titled “Request”| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Lowercased for you. Must match ^[a-z0-9][a-z0-9._-]{0,62}$: lowercase letters, digits, dot, dash, underscore, first character alphanumeric, 63 characters max. |
source | string | yes | One self-contained HTML document with inline CSS and Liquid expressions. Parsed at create time; bad Liquid is rejected here rather than at render time. |
engine | string | no | chromium (default) or satori. |
schema | object | no | JSON Schema (draft 2020-12) for the data payload. Compiled at create time; an invalid schema is rejected. Defaults to {}, which accepts anything. |
options | object | no | Default render options for this template. Per-request options are merged over them. |
expected_pages | integer | no | How many PDF pages a typical payload renders. Default 1, max 2000. This is the estimate the free tier and the spend cap are checked against before the render starts, so declare it on anything that runs long. It is not a limit — the render is metered on the pages it actually produced, and an under-estimate is corrected from that count. |
example | unknown | no | An example payload. Returned by GET /v1/templates/:ref and by the validate endpoint. |
description | string | no | One line, shown in listings. |
message | string | no | A changelog line recorded against this version. |
Engines
Section titled “Engines”chromium | satori | |
|---|---|---|
| Output | PDF, PNG, JPG | PNG |
| CSS | Full HTML and CSS, including @page | Flexbox subset, inline styles, explicit display on every element |
| Fonts | Webfonts and @font-face over https | Bundled Inter (400, 600, 700) only |
| Speed | Hundreds of milliseconds to seconds | Milliseconds |
| Good for | Documents, anything multi-page, anything PDF | OG images, social cards, badges |
A satori template asked for a PDF or JPG is rendered through Chromium instead, using the same
markup — the engine choice is a fast path, not a restriction.
Response
Section titled “Response”201 Created, with the full template object including the new version. Same shape as
GET /v1/templates/:ref.
| Field | Type | Description |
|---|---|---|
object | string | Always template. |
id | string | tpl_…. |
name, description | string | As submitted (name lowercased). |
latest_version | number | 1. |
version, ref | number, string | 1 and name@1. |
engine, schema, options, expected_pages, example, source | As stored. | |
checksum | string | SHA-256 over engine, source, schema and options. Half of the render cache key. |
message | string | null | |
created_at, updated_at, version_created_at | string | ISO 8601. |
Examples
Section titled “Examples”A packing slip
Section titled “A packing slip” Create a template shell
curl -sS https://api.galleyrender.com/v1/templates \
-H "Authorization: Bearer $GALLEY_API_KEY" \
-H 'content-type: application/json' \
-d '{
"name": "delivery-note",
"engine": "chromium",
"description": "One-page delivery note.",
"message": "first cut",
"options": { "page_size": "A4", "margin": "18mm" },
"source": "<html><head><style>@page{size:A4;margin:18mm}body{font:14px/1.5 system-ui}th{text-align:left}</style></head><body><h1>Delivery note {{ reference }}</h1><p>{{ customer.name }} — {{ shipped_on | date_medium }}</p><table><tr><th>Item</th><th>Qty</th></tr>{% for line in items %}<tr><td>{{ line.sku }}</td><td>{{ line.quantity }}</td></tr>{% endfor %}</table></body></html>",
"schema": {
"type": "object",
"required": ["reference", "customer", "items"],
"properties": {
"reference": { "type": "string", "examples": ["DN-8841"] },
"shipped_on": { "type": "string", "format": "date" },
"customer": {
"type": "object",
"required": ["name"],
"properties": { "name": { "type": "string", "examples": ["Acme Robotics"] } }
},
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["sku", "quantity"],
"properties": {
"sku": { "type": "string", "examples": ["WIDGET-01"] },
"quantity": { "type": "number", "minimum": 1, "examples": [2] }
}
}
}
}
},
"example": { "reference": "DN-8841", "shipped_on": "2026-09-16", "customer": { "name": "Acme Robotics" }, "items": [{ "sku": "WIDGET-01", "quantity": 2 }] }
}' Create a template javascript
// Node 22+. No dependencies — `fetch` is built in.
const res = await fetch("https://api.galleyrender.com/v1/templates", {
method: "POST",
headers: {
authorization: `Bearer ${process.env.GALLEY_API_KEY}`,
"content-type": "application/json",
},
body: JSON.stringify({
name: "delivery-note",
engine: "chromium",
description: "One-page delivery note.",
message: "first cut",
options: {
page_size: "A4",
margin: "18mm"
},
source: "<html><head><style>@page{size:A4;margin:18mm}body{font:14px/1.5 system-ui}th{text-align:left}</style></head><body><h1>Delivery note {{ reference }}</h1><p>{{ customer.name }} — {{ shipped_on | date_medium }}</p><table><tr><th>Item</th><th>Qty</th></tr>{% for line in items %}<tr><td>{{ line.sku }}</td><td>{{ line.quantity }}</td></tr>{% endfor %}</table></body></html>",
schema: {
type: "object",
required: [
"reference",
"customer",
"items"
],
properties: {
reference: {
type: "string",
examples: [
"DN-8841"
]
},
shipped_on: {
type: "string",
format: "date"
},
customer: {
type: "object",
required: [
"name"
],
properties: {
name: {
type: "string",
examples: [
"Acme Robotics"
]
}
}
},
items: {
type: "array",
minItems: 1,
items: {
type: "object",
required: [
"sku",
"quantity"
],
properties: {
sku: {
type: "string",
examples: [
"WIDGET-01"
]
},
quantity: {
type: "number",
minimum: 1,
examples: [
2
]
}
}
}
}
}
},
example: {
reference: "DN-8841",
shipped_on: "2026-09-16",
customer: {
name: "Acme Robotics"
},
items: [
{
sku: "WIDGET-01",
quantity: 2
}
]
}
}),
});
const template = await res.json();
if (!res.ok) throw new Error(template.error.message);
console.log(template.ref); Create a template python
# Python 3.9+. Standard library only.
import json, os, urllib.request
body = json.dumps({
"name": "delivery-note",
"engine": "chromium",
"description": "One-page delivery note.",
"message": "first cut",
"options": {
"page_size": "A4",
"margin": "18mm"
},
"source": "<html><head><style>@page{size:A4;margin:18mm}body{font:14px/1.5 system-ui}th{text-align:left}</style></head><body><h1>Delivery note {{ reference }}</h1><p>{{ customer.name }} — {{ shipped_on | date_medium }}</p><table><tr><th>Item</th><th>Qty</th></tr>{% for line in items %}<tr><td>{{ line.sku }}</td><td>{{ line.quantity }}</td></tr>{% endfor %}</table></body></html>",
"schema": {
"type": "object",
"required": [
"reference",
"customer",
"items"
],
"properties": {
"reference": {
"type": "string",
"examples": [
"DN-8841"
]
},
"shipped_on": {
"type": "string",
"format": "date"
},
"customer": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"examples": [
"Acme Robotics"
]
}
}
},
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": [
"sku",
"quantity"
],
"properties": {
"sku": {
"type": "string",
"examples": [
"WIDGET-01"
]
},
"quantity": {
"type": "number",
"minimum": 1,
"examples": [
2
]
}
}
}
}
}
},
"example": {
"reference": "DN-8841",
"shipped_on": "2026-09-16",
"customer": {
"name": "Acme Robotics"
},
"items": [
{
"sku": "WIDGET-01",
"quantity": 2
}
]
}
}).encode()
req = urllib.request.Request(
"https://api.galleyrender.com/v1/templates",
data=body,
headers={
"Authorization": f"Bearer {os.environ['GALLEY_API_KEY']}",
"Content-Type": "application/json",
},
)
template = json.load(urllib.request.urlopen(req))
print(template["ref"]) Create a template — no key shell + json
# No API key. The first call mints a 50-render trial.
curl -sS https://mcp.galleyrender.com/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "create_template",
"arguments": {
"name": "delivery-note",
"engine": "chromium",
"description": "One-page delivery note.",
"message": "first cut",
"options": {
"page_size": "A4",
"margin": "18mm"
},
"source": "<html><head><style>@page{size:A4;margin:18mm}body{font:14px/1.5 system-ui}th{text-align:left}</style></head><body><h1>Delivery note {{ reference }}</h1><p>{{ customer.name }} — {{ shipped_on | date_medium }}</p><table><tr><th>Item</th><th>Qty</th></tr>{% for line in items %}<tr><td>{{ line.sku }}</td><td>{{ line.quantity }}</td></tr>{% endfor %}</table></body></html>",
"schema": {
"type": "object",
"required": [
"reference",
"customer",
"items"
],
"properties": {
"reference": {
"type": "string",
"examples": [
"DN-8841"
]
},
"shipped_on": {
"type": "string",
"format": "date"
},
"customer": {
"type": "object",
"required": [
"name"
],
"properties": {
"name": {
"type": "string",
"examples": [
"Acme Robotics"
]
}
}
},
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": [
"sku",
"quantity"
],
"properties": {
"sku": {
"type": "string",
"examples": [
"WIDGET-01"
]
},
"quantity": {
"type": "number",
"minimum": 1,
"examples": [
2
]
}
}
}
}
}
},
"example": {
"reference": "DN-8841",
"shipped_on": "2026-09-16",
"customer": {
"name": "Acme Robotics"
},
"items": [
{
"sku": "WIDGET-01",
"quantity": 2
}
]
}
}
}
}' { "object": "template", "id": "tpl_6q1wv8k4m2nt", "name": "delivery-note", "description": "One-page delivery note.", "latest_version": 1, "created_at": "2026-09-16T15:02:41.119Z", "updated_at": "2026-09-16T15:02:41.240Z", "version": 1, "ref": "delivery-note@1", "engine": "chromium", "schema": { "type": "object", "required": ["reference", "customer", "items"], "properties": {} }, "options": { "page_size": "A4", "margin": "18mm" }, "example": { "reference": "DN-8841" }, "source": "<html><head><style>@page{size:A4;margin:18mm}…", "checksum": "9c41e7b0a2d8f513…", "message": "first cut", "version_created_at": "2026-09-16T15:02:41.240Z"}Render it with {"template": "delivery-note@1", "data": {…}}.
A name that is taken
Section titled “A name that is taken”curl -sS https://api.galleyrender.com/v1/templates \ -H "Authorization: Bearer $GALLEY_API_KEY" \ -H 'content-type: application/json' \ -d '{ "name": "delivery-note", "source": "<p>{{ reference }}</p>" }'{ "error": { "type": "conflict", "message": "Template `delivery-note` already exists. POST a new version instead.", "docs_url": "https://galleyrender.com/docs/errors/conflict", "details": { "template": "delivery-note", "latest_version": 1, "new_version_url": "/v1/templates/delivery-note/versions" } }}Writing the source
Section titled “Writing the source”- One document. Filesystem includes and partials are disabled, so
{% include %}and{% render %}do not work: everything is either in the string or at a public https URL. - Liquid with two extra filters:
money({{ total | money }}, optional currency and locale arguments) anddate_medium({{ issued_on | date_medium }}). - Unknown filters are a hard error — at render time you get
render_failednaming the filter. Unknown variables are not; they render empty. - Images and fonts are fetched through an SSRF-safe fetcher. Public https URLs
and
data:URIs only.
Errors
Section titled “Errors”| Type | Status | When |
|---|---|---|
invalid_request | 400 | Body is not JSON; name missing or not matching the pattern; source missing or empty; engine not chromium or satori; source is not valid Liquid; schema is not an object or not a compilable JSON Schema. |
conflict | 409 | The name is already used on this account. |
authentication_error | 401 | Missing, unknown or revoked key. |
{ "error": { "type": "invalid_request", "message": "Template source is not valid Liquid: tag \"{% for line in items %}\" not closed", "docs_url": "https://galleyrender.com/docs/errors/invalid_request", "errors": [ { "path": "source", "message": "tag \"{% for line in items %}\" not closed", "expected": "HTML with valid Liquid expressions, e.g. {{ invoice.total | money }}", "example": "<h1>{{ title }}</h1>" } ] }}See also
Section titled “See also”- POST /v1/templates/:name/versions — publish a change
- POST /v1/templates/:ref/validate — dry-run a payload
- Images, fonts and other assets