Skip to content

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/templates
Authorization: Bearer glr_sk_…
Content-Type: application/json

Use 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.

FieldTypeRequiredDescription
namestringyesLowercased for you. Must match ^[a-z0-9][a-z0-9._-]{0,62}$: lowercase letters, digits, dot, dash, underscore, first character alphanumeric, 63 characters max.
sourcestringyesOne self-contained HTML document with inline CSS and Liquid expressions. Parsed at create time; bad Liquid is rejected here rather than at render time.
enginestringnochromium (default) or satori.
schemaobjectnoJSON Schema (draft 2020-12) for the data payload. Compiled at create time; an invalid schema is rejected. Defaults to {}, which accepts anything.
optionsobjectnoDefault render options for this template. Per-request options are merged over them.
expected_pagesintegernoHow 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.
exampleunknownnoAn example payload. Returned by GET /v1/templates/:ref and by the validate endpoint.
descriptionstringnoOne line, shown in listings.
messagestringnoA changelog line recorded against this version.
chromiumsatori
OutputPDF, PNG, JPGPNG
CSSFull HTML and CSS, including @pageFlexbox subset, inline styles, explicit display on every element
FontsWebfonts and @font-face over httpsBundled Inter (400, 600, 700) only
SpeedHundreds of milliseconds to secondsMilliseconds
Good forDocuments, anything multi-page, anything PDFOG 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.

201 Created, with the full template object including the new version. Same shape as GET /v1/templates/:ref.

FieldTypeDescription
objectstringAlways template.
idstringtpl_….
name, descriptionstringAs submitted (name lowercased).
latest_versionnumber1.
version, refnumber, string1 and name@1.
engine, schema, options, expected_pages, example, sourceAs stored.
checksumstringSHA-256 over engine, source, schema and options. Half of the render cache key.
messagestring | null
created_at, updated_at, version_created_atstringISO 8601.
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
            }
          ]
        }
      }
    }
  }'
201 Created (source abbreviated)
{
"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": {…}}.

Create it again
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>" }'
409 Conflict
{
"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"
}
}
}
  • 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) and date_medium ({{ issued_on | date_medium }}).
  • Unknown filters are a hard error — at render time you get render_failed naming 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.
TypeStatusWhen
invalid_request400Body 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.
conflict409The name is already used on this account.
authentication_error401Missing, unknown or revoked key.
400 Bad Request — bad Liquid
{
"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>"
}
]
}
}