# Emails API

Author, revise and retire library emails; filter by where each is used; and the scratch/shared/copy contract that decides whose sends a later edit reaches.

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](https://mimeohq.com/docs/api/)).

## 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 {#title}

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 {#preview}

`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](https://mimeohq.com/docs/api/templates#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. |
| `flow_id` | Numeric ID of a Subscription renewal approaching flow. Resolves `automation.subscription` for the selected person. See below. |
| `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.

### Previewing a bound renewal email {#renewal-preview}

```
POST /api/v1/emails/42/preview
{
  "person_id": 17,
  "flow_id": 9
}
```

Use the saved renewal flow's numeric `flow_id` together with a specific `person_id`. The flow's product and interval select one eligible native subscription; Mimeo never chooses an arbitrary row when multiple non-ended rows match. The preview uses that binding for [`automation.subscription`](https://mimeohq.com/docs/usage/emails#renewal-personalization), `automation.target_at` and `automation.days_before`. Renewal calendar dates use the trigger timezone.

Content preview is **not limited to the upcoming list's 14 days**: a valid yearly renewal months away can supply the amount and date. Other eligibility and timing checks still apply: a target whose local send day has ended cannot supply a binding. Neither can cancelled, ambiguous or otherwise ineligible subscriptions. Missing amounts are not treated as zero.

If the selected person exists but the supplied flow cannot resolve an eligible renewal binding, the preview still renders but `automation` values are blank; that is not a `422` binding error. Check the rendered amount and date explicitly. A successful preview response alone does not prove the subscription is eligible for enrollment.

The response remains `{ html, person }`. Nothing is saved, enrolled, queued or sent; a draft flow stays draft. The [upcoming-reminder preview](https://mimeohq.com/docs/api/flows#preview-renewal-approaching) answers who is approaching a reminder instead. MCP `preview_email` accepts the same `flow_id`. Do not assume the separate test-send endpoint supplies this binding.

## Proving it actually sends {#test-send}

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 {#used-in}

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 {#email-source}

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](https://mimeohq.com/docs/api/sequences) · [Broadcasts API](https://mimeohq.com/docs/api/broadcasts) · [Emails, for humans](https://mimeohq.com/docs/usage/emails)
