# Templates API

Layouts and components — the HTML and CSS an email is dressed in. Create, read, edit, retire and delete them without opening a browser.

All endpoints need a bearer token (see [Overview & auth](/content/docs/api/index.html)). The same abilities are on the [MCP server](/content/docs/mcp/index.html) as `list_templates`, `get_template`, `save_template`, `make_default_layout`, `archive_template` and `delete_template`.

## Layouts and components

A **layout** is the whole document an email is wrapped in: the `<html>`, the header, the footer, the unsubscribe link. It must contain the `{{ content }}` slot, which is where the compiled body is injected. Its `css` is inlined into a `<style>` tag at compile time. One layout is the instance default — the one an email without a layout of its own gets.

A **component** is a reusable fragment — a signature, a promo block — inserted into an email's markdown as `{% component "key" %}`. It carries a `key` (lowercase letters, numbers, dashes) and no CSS of its own; the layout around it supplies that.

A layout also picks a **theme** — the colours, font stacks and page shape its CSS is written against. The `theme` field takes a theme's key. `null` — the default — means "follow whichever theme is the instance default", so promoting a different theme reaches the layout without touching it. Every layout comes back carrying both `theme` (the key it chose, or `null`) and `resolved_theme` (the key it actually renders with). A component has no theme: it's expanded into the body, and the body is wrapped in a layout, which has one. See [the Themes API](/content/docs/api/themes.html#variables/index.html) for the variables and how to edit them.

## Endpoints

| Method & path | What it does |
| --- | --- |
| `GET /api/v1/templates` | Every active layout and component. Filters: `kind` (`layout` or `component`), `archived` (any value includes archived ones). Markup is left out of the list — read one to get it. |
| `POST /api/v1/templates` | Create one. Body: `name`, `kind`, `key` (components), `html`, `css`, `theme` (layouts). Omit `html` and a layout starts from a working default that already has the slot; omit `css` and it starts from a matching stylesheet — one column at a readable measure, and every element an email uses already styled. That starter is written entirely against [theme](/content/docs/api/themes.html) tags: every colour, font and size in it is a `{{ theme.… }}` tag rather than a literal, with a `@media (prefers-color-scheme: dark)` block carrying the dark counterpart of each colour. |
| `GET /api/v1/templates/:id` | One template with its `html` and `css` — and, for a layout, its `theme` and `resolved_theme`. A component may be addressed by its key instead of its id. |
| `PATCH /api/v1/templates/:id` | Update `name`, `key`, `html`, `css` or `theme`. `kind` is ignored — a layout never becomes a component. Send `is_default: true` to make a layout the default, or `archived: true`/`false` to retire or restore it. |
| `DELETE /api/v1/templates/:id` | Delete it. Refused with `409` for the default layout — make another layout the default first. |

## Creating a layout

```
curl -X POST https://your-instance.example.com/api/v1/templates \
  -H "Authorization: Bearer $MIMEO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Newsletter",
    "kind": "layout",
    "html": "<div class=\"wrap\">{{ content }}</div>\n<p>{{ mailing_address }}<br><a href=\"{{ unsubscribe_url }}\">Unsubscribe</a></p>",
    "css": ".wrap { max-width: {{ theme.content_max_width }}; margin: 0 auto; color: {{ theme.body_text_color }}; }",
    "theme": "brand-b"
  }'
```

`\n` is how a multi-line document goes into a JSON string — the markup is stored exactly as sent, newlines and all.

The response is the stored template:

```
{
  "template": {
    "id": 7,
    "name": "Newsletter",
    "kind": "layout",
    "key": null,
    "is_default": false,
    "archived": false,
    "theme": "brand-b",
    "resolved_theme": "brand-b",
    "updated_at": "2026-08-02T12:00:00Z",
    "html": "<div class=\"wrap\">{{ content }}</div>\n…",
    "css": ".wrap { max-width: {{ theme.content_max_width }}; … }"
  }
}
```

The markup and the stylesheet are stored exactly as written, theme tags and all — the values are substituted when an email is compiled, not when the layout is saved. Send "theme": null instead and the layout follows the default theme, which is what `resolved_theme` would then name.

## Theme tags

A theme tag is a value from a [theme](/content/docs/api/themes.html) written into a layout's `css` (or its `html`). Four forms:

```
{{ theme.body_text_color }}               the layout's theme, light value
{{ theme.dark.body_text_color }}          the layout's theme, dark value
{{ theme.brand-b.body_text_color }}       another theme, named by key
{{ theme.brand-b.dark.body_text_color }}  another theme, dark value
```

They are resolved at **compile time, before Liquid runs** — substituted into the stored source, so the renderer only ever sees finished values. A theme tag isn't Liquid; it only borrows the syntax, which is why it works in a stylesheet as readily as in the markup.

A button variable that doesn't name an emphasis means the primary one: `{{ theme.button_background_color }}` and `{{ theme.button_primary_background_color }}` are the same value. A tag naming a variable that doesn't exist is refused with `422` when the layout is saved, with the bad tag named — a typo can't quietly compile to a hole.

**The CSS is still yours.** Replace a theme tag with a literal value and that one declaration simply stops following the theme, while the rest keep going. A layout written with no theme tags at all is a perfectly good layout — it just won't answer to a theme.

## Liquid available in a layout

| Tag | What it resolves to |
| --- | --- |
| `{{ content }}` | The compiled email body. **Required** — a layout without it is refused. Any inner spacing works: `{{content}}` and `{{ content }}` are the same slot. |
| `{{ unsubscribe_url }}` | A signed, per-recipient unsubscribe link. Put it in the footer of every layout. |
| `{{ mailing_address }}` | The account's physical mailing address, from [settings](/content/docs/api/settings.html) (`sending.mailing_address`). Put it in the footer too — anti-spam law requires marketing email to show one. Resolves at send time, so changing the setting changes every future send. |
| `{{ theme.<variable> }}` | A value from the layout's theme — `{{ theme.link_color }}`, `{{ theme.content_max_width }}`. `{{ theme.dark.<variable> }}` is the dark half of a colour, and `{{ theme.<key>.<variable> }}` reads another theme by key. Not really Liquid: these resolve first, before the renderer runs, which is why they work in the `css` too. [Theme tags](/content/docs/api/templates#theme-tags/index.html). |

The stylesheet is rendered as Liquid alongside the markup once the theme tags are substituted, so any other Liquid syntax written into the CSS — even inside a comment — is a tag the renderer will try to run, and an unknown one stops the render.

A layout brought over from another tool usually carries that tool's slot name — `{{ message_content }}` is the common one. Rename it to `{{ content }}`; the validation error names the slot it found so you don't have to hunt for it.

## Making a layout the default

```
curl -X PATCH https://your-instance.example.com/api/v1/templates/7 \
  -H "Authorization: Bearer $MIMEO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"is_default": true}'
```

There is exactly one default layout: the previous holder steps down in the same request. Archiving the default also stands it down, so another active layout takes over.

## Retire or delete

`archived: true` is the gentler retirement — emails already built on the template keep rendering, and it stops being offered for new work. Restore with `archived: false`. `DELETE` removes it outright; emails pointing at a deleted layout fall back to the default.

Saving a template's `html` or `css` recompiles every email built on it, so a fix to a layout reaches drafts and queued sends without touching them one at a time. Saving the theme a layout points at does the same thing, which is how one colour change reaches every email at once.

## Errors

| Status | When |
| --- | --- |
| `422` | A layout with no `{{ content }}` slot, a theme tag naming a variable that doesn't exist, a component with a missing, malformed or already-taken `key`, or a missing `name`. The `errors` array names the field — or the bad tag — and the fix. |
| `409` | Deleting the default layout. Make another layout the default first. |
| `404` | No template with that id or key.
