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). The same
abilities are on the MCP server 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, 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:
.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:
{
"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:
{
"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.
Don't hard-code this list. GET /api/v1/themes
returns the vocabulary the instance is actually running, defaults and
all. Read it at the start of a session and you can't drift out of date
with a variable added after this page was written.
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:
{{ 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:
.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:
{ "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:
{% 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:
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:
{
"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.
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 carries a
theme field, which takes a theme key:
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 · MCP