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:

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. One
source: 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