Emails API — Mimeo docs
Emails API
The email library: author, revise and retire emails, sort out what's actually reusable, and split a shared email off for one holder.
All endpoints need a bearer token (see Overview & auth).
Endpoints
| Method & path | What it does |
|---|---|
GET /api/v1/emails |
The library. Filters below. |
POST /api/v1/emails |
Create one. Body: title, subject, pre_header, from_address, body_markdown, labels[], template_id. |
GET /api/v1/emails/:id |
One email with its markdown body and everywhere it's used. |
PATCH /api/v1/emails/:id |
Update any of the same fields. |
DELETE /api/v1/emails/:id |
Archives it by default. An email that has been sent is part of the record of what went out, and the send log has to keep pointing at it. With ?permanent=1 the row is permanently deleted instead — allowed only for an email nothing points at; anything held by the send log, the queue, a sequence, a broadcast or a flow refuses with 409 and a list of what's holding it. |
POST /api/v1/emails/:id/preview |
The compiled, personalized HTML for one person. Optional body_markdown / template_id render a draft that is never saved. See below. |
POST /api/v1/emails/:id/test_send |
Deliver it for real, to one address. Body: to (required). See below. |
POST /api/v1/emails/fork |
Split one holder off a shared email. Body: holder_type (sequence_step or broadcast), holder_id. |
Title and subject
An email carries two names. subject is the subject line, as recipients read it. title is what the email is called wherever it's managed rather than read — the library list, a sequence's steps, a picker — and never reaches a recipient. Set one when the subject makes a bad label: "Try Builder Methods Pro again ($50 off as a returning member)" is a fine subject and a useless row in a list of five steps.
Reading, title always answers "what is this email called" — the subject stands in when nobody has set one, so it is never blank. title_raw is what's actually stored, and is null for an email that has never been named, which is how you tell "named this" from "falling back". Writing, send title; an empty string clears it and hands the job back to the subject.
GET /api/v1/emails/42
{ "email": { "id": 42,
"title": "BMP winback",
"title_raw": "BMP winback",
"subject": "Try Builder Methods Pro again ($50 off as a returning member)",
… } }
GET /api/v1/emails/43 # never given a title
{ "email": { "id": 43,
"title": "Where to start",
"title_raw": null,
"subject": "Where to start",
… } }
Reading back what you wrote
POST /api/v1/emails/:id/preview returns the email as it would land: components expanded, {% button %} rendered in the theme's colours, markdown converted, the layout's theme tags resolved, the layout wrapped around it and Liquid resolved against one real person. The theme tags go first, at compile time, before Liquid runs at all — so what a preview shows is the theme the layout actually points at. Tracking is the one thing a preview leaves out — no wrapped links, no open pixel — because a preview is never a send.
POST /api/v1/emails/42/preview
{ "person_id": 17 }
{ "html": "<!doctype html>…", "person": { "id": 17, "email": "pat@example.com", "name": "Pat" } }
| Field | What it does |
|---|---|
person_id |
Who to render for. Omitted, it's a pinned preview person, or anyone. |
segment_id |
Render for somebody who would actually receive this: a member of that segment, at random. Their merge fields are the real ones. |
mode |
random for anyone at all — useful for catching a missing field your own tidy test record would never hit. |
body_markdown |
Render this body instead of the saved one. Nothing is written. |
template_id |
Render through this layout instead of the saved one. Nothing is written. |
The last two are the check-before-write: whether a component expands, whether a button renders, whether a conditional resolves the way it was meant to — answered without the email having to carry the draft first. The record is untouched, and so is its cached compile, so a preview of a half-written body can never be what a later send goes out with.
422 when there is nobody to preview as. A preview needs a real person, because rendering against an imaginary one is how a broken {{ person.first_name }} gets through.
Proving it actually sends
A preview answers what the email says. POST /api/v1/emails/:id/test_send answers whether it arrives: the compiled email, through the configured provider, into a real inbox. It's the only check that exercises the provider, the sending domain and whatever the receiving client does with the HTML.
POST /api/v1/emails/42/test_send
{ "to": "reader@example.com" }
{ "sent": true, "to": "reader@example.com", "provider": "postmark",
"note": "Accepted by the provider. Acceptance isn't delivery." }
to is required. An API token isn't a person, so unlike the editor there is no "me" to fall back to — and a real message in a real inbox deserves a deliberate address rather than a default. Anything that isn't an address is a 422.
Two consequences worth knowing before you call it:
- It's marked as a test, so nobody's sent-status moves and nobody's place in a sequence changes. Sending one doesn't make a recipient ineligible for the real thing later.
- The address becomes a person if it isn't one already, because a send needs somebody to send to. Testing against an address is a small, deliberate way of adding it.
The response says sent, not delivered. The provider accepting a message is not the inbox receiving it, and reporting otherwise is how a bounce goes unnoticed.
Telling reusable content from per-broadcast rows
Every broadcast mints its own email row — that's deliberate, and it means the library fills up with rows that existed for exactly one send. Two questions sort it out, and neither is answerable by search:
GET /api/v1/emails?used_in[]=sequences&used_in[]=flows&used_mode=none
GET /api/v1/emails?used_in[]=broadcasts&used_mode=any
| Facet | Matches an email that… |
|---|---|
broadcasts |
a broadcast points at |
sequences |
a sequence step points at |
flows |
a flow reaches at all — a send_email step naming it, or a start_sequence step pointing at a sequence that contains it |
flows_one_off |
a flow's own send_email step names directly |
used_mode is any (in any of them, the default), all (in all of them) or none (in none of them). Filtering happens server-side before any limit. Unknown facets are ignored rather than filtering everything away.
Every row also carries where_used, so you can see what editing an email would reach without asking a second question. Other filters: ?q= searches title, subject and pre-header, ?label= matches a label, ?archived=1 includes archived rows, ?limit= caps at 200.
The email-source contract
Anywhere an email is attached to something — a sequence step, a broadcast, or either one's replace-email endpoint — the request carries email_source. It is required, with no default.
| Value | What it means after the fact |
|---|---|
scratch |
A blank new row in the library. Nothing else points at it, so nothing else is affected by it. |
shared |
The library email named by email_id, itself. Onesource: a later edit reaches every send that uses it, all its opens and clicks accrue to the one email, and anyone who received it anywhere counts as having received it everywhere. |
copy |
An independent copy of it, subject kept. Edits stay here, its stats start at zero, and everyone who already received the original counts as not having received this one — so they're eligible again. |
Those consequences are the whole reason the choice exists, which is why there is no default: sharing and forking are opposite answers to "what does my next edit change", and picking one for the caller means they find out which way we picked after the send.
A broadcast never shares
POST /api/v1/broadcasts and its replace-email endpoint accept only scratch and copy. shared is refused with 422, on the server, rather than trusted to a UI that only offers two options. What one broadcast sent should stay what it sent — a shared row would mean editing a sequence step could silently rewrite the record of a send that already went out.
Forking: the repair
When something is shared that should have been a copy, POST /api/v1/emails/fork splits that one holder onto a row of its own. Every other holder keeps the original. Three consequences, all at once: edits stop reaching everywhere else, the copy's stats start at zero, and everyone who received the original counts as not having received this one.
POST /api/v1/emails/fork
{ "holder_type": "sequence_step", "holder_id": 12 }
A copy made for one holder keeps the subject. Only the library's own Duplicate prepends "Copy of" — there, two identical rows in a list nobody can tell apart is the actual problem. Attached to a step, the subject is what recipients see, and nobody means to send them "Copy of".
Errors
| Status | When |
|---|---|
401 |
Missing or revoked token. |
404 |
No such email. |
422 |
Validation failed, or an email source that would be a guess. The message names the three choices and what each means. |
409 |
DELETE ?permanent=1 on an email something still points at. The errors name the holders; archive instead, or detach them first. |
See also: Sequences API · Broadcasts API · Emails, for humans