Templates
Docs / Control plane

Templates

List and read templates, and every version each one has, newest first. Publishing stays in the panel, or a push to the repository you connected. The API does not mint versions.

Authentication

Authorization: Bearer spw_…

Requires templates:read (or templates:manage).

Keys rule — prefix, shown once, abilities never widen, and no key carries both messages:send and messages:read-content.

Endpoints

GET/v1/templates — list · ?source · ?limit · ?cursor
GET/v1/templates/{id} — detail + versions[]: every version, newest first, each with its git_sha
GET/v1/templates/by-slug/{slug} — same detail; slug is per-account

Fields

sourcepanel when the template was created in the panel, git when a sync wrote it from the connected repository.
engineOn each version. twig or liquid. The file extension decides it: liquid stays liquid, and twig, html and txt are twig. A sidecar engine that disagrees fails the sync.
is_sensitiveWhen true, the send stores no subject and seals the body. Opening a sealed body is limited to the account owner. See sealed bodies.
variables_schemaOn each version. A send that omits a name listed under required is 422, and the message names every missing one. Extra names are refused only when the schema names the allowed set, as a vars or properties map, or with additionalProperties set to false.
git_shaOn each version. The last commit that touched the file, or null when the template came from the panel or the body was not committed. A message response does not include it. GET /v1/messages/{id} returns template_version_id, and that id is versions[].id here. The message list does not include that field.
Show and by-slug
{
  "data": {
    "id": 1,
    "slug": "welcome",
    "name": "Welcome",
    "description": null,
    "source": "panel",
    "is_sensitive": false,
    "content_retention_days": null,
    "current_version": {
      "id": 1,
      "version": 1,
      "published_at": "2026-08-06T08:11:01+00:00"
    },
    "versions": [
      {
        "id": 1,
        "version": 1,
        "subject": "Hello {{ name }}",
        "body_html": "<p>Hello {{ name }}</p>",
        "body_text": "Hello {{ name }}",
        "engine": "twig",
        "variables_schema": {
          "required": [
            "name"
          ]
        },
        "git_sha": null,
        "published_at": "2026-08-06T08:11:01+00:00",
        "created_at": "2026-08-06T08:11:01+00:00"
      }
    ]
  }
}

List

A list row stops at current_version. versions[] is on the object above.

curl
curl https://spoolway.com/api/v1/templates \
  -H "Authorization: Bearer spw_…"
PHP
$ch = curl_init("https://spoolway.com/api/v1/templates");
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("SPOOLWAY_KEY")],
]);
$page = json_decode(curl_exec($ch), true);
Node
const res = await fetch("https://spoolway.com/api/v1/templates", {
  headers: { Authorization: `Bearer ${process.env.SPOOLWAY_KEY}` },
});
const page = await res.json();
Python
import os, requests

page = requests.get(
    "https://spoolway.com/api/v1/templates",
    headers={"Authorization": f"Bearer {os.environ['SPOOLWAY_KEY']}"},
).json()
Ruby
require "net/http"; require "json"

uri = URI("https://spoolway.com/api/v1/templates")
req = Net::HTTP::Get.new(uri,
  { "Authorization" => "Bearer #{ENV["SPOOLWAY_KEY"]}" })
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
page = JSON.parse(res.body)
200 list
{
  "data": [
    {
      "id": 1,
      "slug": "welcome",
      "name": "Welcome",
      "description": null,
      "source": "panel",
      "is_sensitive": false,
      "content_retention_days": null,
      "current_version": {
        "id": 1,
        "version": 1,
        "published_at": "2026-08-06T08:11:01+00:00"
      }
    }
  ],
  "next_cursor": null
}

Show

GET /v1/templates/{id} returns the object above.

curl
curl https://spoolway.com/api/v1/templates/1 \
  -H "Authorization: Bearer spw_…"
PHP
$ch = curl_init("https://spoolway.com/api/v1/templates/" . $id);
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("SPOOLWAY_KEY")],
]);
$template = json_decode(curl_exec($ch), true);
Node
const res = await fetch(`https://spoolway.com/api/v1/templates/${id}`, {
  headers: { Authorization: `Bearer ${process.env.SPOOLWAY_KEY}` },
});
const template = await res.json();
Python
import os, requests

template = requests.get(
    f"https://spoolway.com/api/v1/templates/{id}",
    headers={"Authorization": f"Bearer {os.environ['SPOOLWAY_KEY']}"},
).json()
Ruby
require "net/http"; require "json"

uri = URI("https://spoolway.com/api/v1/templates/#{id}")
req = Net::HTTP::Get.new(uri,
  { "Authorization" => "Bearer #{ENV["SPOOLWAY_KEY"]}" })
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
template = JSON.parse(res.body)

By slug

Same detail as show, keyed on the per-account slug. The body is the same object.

curl
curl https://spoolway.com/api/v1/templates/by-slug/welcome \
  -H "Authorization: Bearer spw_…"
PHP
$ch = curl_init("https://spoolway.com/api/v1/templates/by-slug/" . rawurlencode($slug));
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("SPOOLWAY_KEY")],
]);
$template = json_decode(curl_exec($ch), true);
Node
const res = await fetch(
  `https://spoolway.com/api/v1/templates/by-slug/${encodeURIComponent(slug)}`,
  { headers: { Authorization: `Bearer ${process.env.SPOOLWAY_KEY}` } },
);
const template = await res.json();
Python
import os, requests

template = requests.get(
    f"https://spoolway.com/api/v1/templates/by-slug/{slug}",
    headers={"Authorization": f"Bearer {os.environ['SPOOLWAY_KEY']}"},
).json()
Ruby
require "net/http"; require "json"

uri = URI("https://spoolway.com/api/v1/templates/by-slug/#{URI.encode_www_form_component(slug)}")
req = Net::HTTP::Get.new(uri,
  { "Authorization" => "Bearer #{ENV["SPOOLWAY_KEY"]}" })
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
template = JSON.parse(res.body)

Responses & failures

403 without the ability. Unknown id or slug on this account is 404. Slugs are per-account — the same slug on another account is a different row.