# Themes API

A theme holds the creative decisions an email is built from — colors,
font stacks, and the shape of the page. Read them, edit them, and point
a layout at one.

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_themes`, `get_theme`, `save_theme`, `make_default_theme` and
delete_theme`.

## What a theme is

Every email that goes out is wrapped in a
[layout](/content/docs/api/templates.html), and a layout is HTML and
CSS. Without a theme, the decisions inside that CSS — the brand blue,
the body font, how wide the content column runs — are literal values
typed into a stylesheet. Change the brand blue and you have to find
every stylesheet that quoted it, and the one you miss keeps sending the
old color.

A theme is where those decisions live instead. The layout's CSS
references them as tags, so the value exists in one place and the
stylesheets follow it:

```css
.email-content {
  max-width: {{ theme.content_max_width }};
  padding: {{ theme.content_padding_top }} {{ theme.content_padding_sides }} {{ theme.content_padding_bottom }};
  background: {{ theme.content_background_color }};
  border: {{ theme.content_border_thickness }} solid {{ theme.content_border_color }};
  border-radius: {{ theme.content_border_radius }};
}

a { color: {{ theme.link_color }}; }
```

The tags resolve at compile time, before Liquid runs — what reaches the
recipient is ordinary CSS with real values in it. An unknown variable is
refused when the layout is saved, and the error names the tag it
couldn't resolve.

Saving a theme recompiles every email built on it, so changing a brand
color reaches drafts already written and sends already queued, without
opening any of them.

## Endpoints

| Method & path | What it does |
| --- | --- |
| `GET /api/v1/themes` | Every theme, plus `variables` — the whole vocabulary, each with its `name`, `type`, `group`, `label`, `help` and `default` — and `button_styles`, the three emphases a button tag may name. |
| `GET /api/v1/themes/:key` | One theme, with its variables **resolved**: every variable comes back with a value whether it was set on this theme or left at its default. |
| `POST /api/v1/themes` | Create one. Body: `name`, optional `variables`, optional `is_default`. The key is derived from the name. |
| `PATCH /api/v1/themes/:key` | Update it. **Partial by default** — only the variables you send change. Send `replace: true` to treat `variables` as the whole set, or `is_default: true` to make it the default. |
| `DELETE /api/v1/themes/:key` | Delete it. Refused with `409` for the default theme, and for a theme any layout is still using. |

A theme is addressable by its key or its numeric id, so
`/api/v1/themes/brand-b` and `/api/v1/themes/4`
reach the same record.

## Reading a theme

The list gives you the vocabulary — what a theme _can_ hold —
alongside the themes themselves. Read it once and you know every variable
name, its type, and what it falls back to:

```json
{
  "themes": [
    { "id": 1, "key": "default",  "name": "Default",  "is_default": true },
    { "id": 4, "key": "brand-b",  "name": "Brand B",  "is_default": false }
  ],
  "variables": [
    {
      "name": "link_color",
      "type": "color",
      "group": "colors",
      "label": "Link color",
      "help": "Links in the body and footer.",
      "default": { "light": "#2563eb", "dark": "#94c6ff" }
    },
    …
  ],
  "button_styles": ["primary", "secondary", "tertiary"]
}
```

Reading one theme resolves its variables, so you never have to merge a
theme against the defaults yourself — a color arrives as a
`light`/`dark` pair and a size as a string,
whether it was set here or inherited:

```json
{
  "theme": {
    "id": 4,
    "key": "brand-b",
    "name": "Brand B",
    "is_default": false,
    "variables": {
      "body_font_stack": "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Arial, sans-serif",
      "link_color": { "light": "#7c3aed", "dark": "#c4b5fd" },
      "content_max_width": "560px"
    }
  }
}
```

## The variables

Three types. A `color` carries a light value and a dark one.
A `length` is CSS — `px`, `rem`, `em` or `%`, and a bare number is read as pixels.
A `font` is a plain font-stack string.

### Typography

| Variable | Type | Default |
| --- | --- | --- |
| `body_font_stack` | `font` | `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif` |
| `headline_font_stack` | `font` | Blank — blank means headlines use the body font |

### Colors

| Variable | Light | Dark | What it paints |
| --- | --- | --- | --- |
| `email_background_color` | `#f3f4f6` | `#101623` | Behind everything |
| `content_background_color` | `#ffffff` | `#1f2637` | The content area itself |
| `heading_color` | `#111827` | `#f9fafb` | Headlines and bold text |
| `body_text_color` | `#1f2937` | `#e5e7eb` | Body copy |
| `muted_text_color` | `#6b7280` | `#9ca3af` | The footer and quoted text |
| `link_color` | `#2563eb` | `#94c6ff` | Links |
| `hr_color` | `#e5e7eb` | `#374151` | Dividers |
| `content_border_color` | `#e5e7eb` | `#47556b` | The border around the content area |

### Buttons

| Variable | Type | Light | Dark |
| --- | --- | --- | --- |
| `button_primary_background_color` | `color` | `#2563eb` | `#a3ceff` |
| `button_primary_text_color` | `color` | `#ffffff` | `#111827` |
| `button_secondary_background_color` | `color` | `#111827` | `#f9fafb` |
| `button_secondary_text_color` | `color` | `#ffffff` | `#111827` |
| `button_tertiary_background_color` | `color` | `#e5e7eb` | `#374151` |
| `button_tertiary_text_color` | `color` | `#111827` | `#f9fafb` |
| `button_border_radius` | `length` | `6px` |

### Layout

| Variable | Type | Default |
| --- | --- | --- |
| `content_max_width` | `length` | `560px` |
| `content_border_thickness` | `length` | `1px` |
| `content_border_radius` | `length` | `8px` |
| `hr_thickness` | `length` | `4px` |
| `content_padding_top` | `length` | `30px` |
| `content_padding_sides` | `length` | `30px` |
| `content_padding_bottom` | `length` | `30px` |
| `content_margin_top` | `length` | `20px` |
| `content_margin_sides` | `length` | `20px` |
| `content_margin_bottom` | `length` | `20px` |

Padding and margin come in threes — top, left-and-right, bottom —
which is the shape of the CSS shorthand they're written into. Left and right
differing is a thing almost nobody wants and everybody would have to skip past.

### Font stacks

`GET /api/v1/themes` also returns `font_stacks`: the
stacks the theme editor offers, each with a `label`, a
`category` (Sans serif, Serif, Monospace) and the
`stack` itself. They are the ones that actually resolve in mail
clients — a webfont that looks right in a browser and falls back to
Times in Outlook is the usual way an email arrives looking broken. Any other
stack can still be set; the list is a shortcut, not a restriction.

## Light and dark

Every color is two decisions, not one: mail clients honor the reader's
system setting, and a palette picked for a white page is unreadable on a
black one. So a color variable carries both values, and there are two
tags for it:

```liquid
{{ theme.body_text_color }}        the light value
{{ theme.dark.body_text_color }}   the dark value
```

Which one you write is a decision about where in the stylesheet you are.
The light values go in the ordinary rules; the dark ones go in a
`@media (prefers-color-scheme: dark)` block:

```css
.email-content {
  background: {{ theme.content_background_color }};
  border-color: {{ theme.content_border_color }};
}

@media (prefers-color-scheme: dark) {
  .email-content {
    background: {{ theme.dark.content_background_color }};
    border-color: {{ theme.dark.content_border_color }};
  }
}
```

A layout can also reach past its own theme and name another one by key —
`{{ theme.brand-b.link_color }}`, or
`{{ theme.brand-b.dark.link_color }}` for its dark value.

Over the API a color is that pair, written as an object:

```json
{ "variables": { "link_color": { "light": "#7c3aed", "dark": "#c4b5fd" } } }
```

## The three button styles

A theme defines three button emphases — `primary`,
`secondary` and `tertiary` — each a background and
a text color, sharing one `button_border_radius`. A button in
an email's markdown names the one it wants:

```liquid
{% button label:"Start your trial" url:"https://example.com/signup" style:"primary" %}
```

Omit `style` and you get the primary one. The same shorthand
works in a stylesheet: a button tag with no emphasis in its name means
the primary, so `{{ theme.button_background_color }}` and
`{{ theme.button_primary_background_color }}` are the same
variable.

The colors come from the theme rather than from the email, so editing a
button color recompiles every email that uses one. A button whose light
and dark colors differ gets a
`@media (prefers-color-scheme: dark)` rule of its own; a
button that looks the same either way gets none, so nothing is emitted
that isn't doing work.

## Partial and replace updates

`PATCH` is partial by default. Send the one variable you're
changing and nothing else moves — the rest of the theme keeps whatever
it had, set or defaulted:

```bash
curl -X PATCH https://your-mimeo.com/api/v1/themes/brand-b \
  -H "Authorization: Bearer $MIMEO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "variables": {
      "link_color": { "light": "#7c3aed", "dark": "#c4b5fd" }
    }
  }'
```

That's the safe default for an agent: it can't quietly reset a variable
it never read. When you do want the payload to be the whole truth — an
import, a theme rebuilt from a file — send
`replace: true` and anything absent from
`variables` goes back to its default:

```json
{
  "replace": true,
  "variables": {
    "link_color": { "light": "#7c3aed", "dark": "#c4b5fd" },
    "content_max_width": "640px"
  }
}
```

Values are checked before anything is stored. A malformed color or an
unusable length is refused with `422` and the field named,
rather than saved and discovered later in someone's inbox.

## The default theme

One theme is the instance default. `is_default: true` on a
`POST` or `PATCH` hands it the title, and the
previous holder steps down in the same request — there is always exactly
one.

```bash
curl -X PATCH https://your-mimeo.com/api/v1/themes/brand-b \
  -H "Authorization: Bearer $MIMEO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"is_default": true}'
```

## Pointing a layout at a theme

A [layout](/content/docs/api/templates.html) carries a
theme field, which takes a theme **key**:

```bash
curl -X PATCH https://your-mimeo.com/api/v1/templates/7 \
  -H "Authorization: Bearer $MIMEO_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"theme": "brand-b"}'
```

Send "theme": null to clear it. That isn't "no theme" — it
means the layout follows whichever theme is the default, so changing the
default reaches it. Because the two cases read differently, a layout also
reports `resolved_theme`: `theme` is the choice,
`resolved_theme` is what it actually renders with.

Components have no theme of their own. A component is expanded into the
body, and the body is wrapped in a layout — so it inherits whatever that
layout uses.

## Errors

| Status | When |
| --- | --- |
| `422` | A missing `name`, an unknown variable name, or a value that isn't valid for its type. The `errors` array names the field. |
| `409` | Deleting the default theme — make another theme the default first — or deleting a theme still in use. The error names the layouts holding it. |
| `404` | No theme with that key or id. |

The in-use check covers both ways a layout can depend on a theme: the
one it points at with `theme`, and any theme its markup names
by key in a `{{ theme.brand-b.… }}` tag. Deleting either would
leave a layout that no longer compiles, so the delete is refused instead
and you're told which layouts to fix.

See also: [Templates API](/content/docs/api/templates.html) ·
[MCP](/content/docs/mcp/index.html)
