Flows API — Mimeo docs
Flows API
Build, check, rehearse and ship a flow without a deploy. Full parity with the UI — this is the API the CLI and MCP server reuse.
All endpoints need a bearer token (see Overview & auth). Flows are addressed by id or key, so an agent that wrote welcome_series doesn't have to look up a number to edit it.
The working loop
GET /api/v1/emailsand/sequencesto resolve the IDs the definition needs.POST /api/v1/flowsto create a draft, orPATCHan existing one.POST .../validateuntil it comes back clean. A definition can be saved while it still has problems, which is what makes iterating possible.POST .../dry_runagainst a real person to see what would happen.POST .../activate. This refuses on errors and records a version automatically.
Endpoints
| Method & path | What it does |
|---|---|
GET /api/v1/flows |
Every flow with its labels, trigger summary, step count, and how many people are inside, plus labels: every label in use. Narrow to one group with ?label=. |
POST /api/v1/flows |
Create a draft. Body: key, name, labels, run_policy, definition. |
GET /api/v1/flows/:id_or_key |
The full definition, every step's ID and one-line summary, live per-step counts, and the current validation result. |
PATCH /api/v1/flows/:id_or_key |
Update name, labels, run_policy, or definition. A field you leave out is left alone. |
DELETE /api/v1/flows/:id_or_key |
Only while nobody has ever run through it. Otherwise 409 — pause it instead, so their history survives. |
POST .../validate |
Check a definition without saving. Pass definition to check an unsaved one, or omit it to check the saved one. |
POST .../dry_run |
Rehearse against a real person. Zero side effects. |
POST .../activate |
Go live. 409 with the validation attached if it isn't clean. |
POST .../pause |
Freeze the flow and hold its queued emails. |
GET .../versions |
Every checkpoint and activation version. |
POST .../versions |
Save a checkpoint. Body: name, description. |
POST .../versions/:version_id/rollback |
Restore a version. Re-validated first when the flow is live. |
Lookups
Read-only, so a definition can reference real records instead of guesses.
| Path | Returns |
|---|---|
GET /api/v1/emails |
Active emails with labels and where each is used. Supports ?q= and ?label=. |
GET /api/v1/sequences |
Sequences with their steps, so you can see what a start_sequence step would actually do. |
GET /api/v1/tags |
Tags with people counts. Tags are created on demand, so this is for spelling rather than permission. |
GET /api/v1/products |
Product keys for money conditions. |
GET /api/v1/queue |
What's about to be sent, what's holding, and why. Filter with ?status= or ?person=. |
Creating a flow
curl -X POST https://your-mimeo.com/api/v1/flows \
-H "Authorization: Bearer $MIMEO_TOKEN" \
-H "Content-Type: application/json" \
-d '{\
"key": "trial_journey",\
"name": "Trial journey",\
"labels": ["onboarding", "revenue"],\
"run_policy": "single_run",\
"definition": {\
"trigger": {\
"event": "signed_up",\
"match": { "all": [{ "detail": "plan", "op": "equals", "value": "trial" }] }\
},\
"nodes": [\
{ "id": 1, "slug": "check-member", "name": "Already a member?", "type": "if_else",\
"condition": { "type": "has_tag", "tag": "member" },\
"then": [\
{ "id": 2, "slug": "member-exit", "name": "Done — already a member", "type": "exit_flow" }\
],\
"else": [] },\
{ "id": 3, "slug": "start-welcome", "name": "Start the welcome sequence",\
"type": "start_sequence", "sequence_id": 3 },\
{ "id": 4, "slug": "wait-buffer", "name": "Quiet buffer", "type": "delay",\
"duration": { "setting": "quiet_buffer_days" } },\
{ "id": 5, "slug": "wait-for-visit", "name": "Wait for a pricing visit", "type": "gate",\
"wait_for": { "event": { "name": "visited_pricing", "occurrence": "next_occurrence" } },\
"expires_after": { "amount": 14, "unit": "days" },\
"met": [],\
"expired": [\
{ "id": 6, "slug": "quiet-exit", "name": "Done — went quiet", "type": "exit_flow" }\
] },\
{ "id": 7, "slug": "send-pitch", "name": "Send the pitch", "type": "send_email", "email_id": 12 }\
]\
}\
}'
Labels
Labels group flows the way you think about them — onboarding, revenue, win-back. They are freeform, created by using them, and change nothing about how a flow runs: they exist so a list of thirty flows can be narrowed to the five that belong together.
GET /api/v1/flows returns each flow's labels alongside a top-level labels array of every label in use — read it before inventing a new one, so you join the existing vocabulary instead of coining a near-duplicate of it. Pass ?label=onboarding to list only the flows carrying that label.
On POST and PATCH, labels replaces the whole set: send every label the flow should carry. [] clears them, and leaving the field out entirely leaves them alone. Blank entries and duplicates are dropped, and surrounding whitespace is trimmed.
# Every flow in one group
curl -H "Authorization: Bearer $MIMEO_TOKEN" \
"https://your-mimeo.com/api/v1/flows?label=onboarding"
# Re-label one flow — this is the whole set, not an addition
curl -X PATCH https://your-mimeo.com/api/v1/flows/trial_journey \
-H "Authorization: Bearer $MIMEO_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "labels": ["onboarding", "revenue"] }'
Emails carry labels of their own, on the same terms — see the emails API. The two vocabularies are separate: a label on a flow and the same word on an email are unrelated.
How the definition is shaped
Order is the routing. Steps run in the order they appear in nodes — the first one is where a person starts, and there are no pointers between steps to keep in sync.
Branches nest, and both branching types have the same shape. A branch carries exactly two arms, each a plain array of steps on the step itself: then and else on an if_else, met and expired on a gate whose expires_after is a duration. A gate that never gives up carries no arms at all — nobody can reach an "expired" path — and it is a plain wait. Spell that as an expires_after of "never"; omitting the field means the same but draws a validator warning, since an omission can't be told from a forgotten field.
When an arm runs out, the person carries on to the step after the branch — so an empty arm means "this side does nothing extra", both arms meet again below, and an exit_flow at the end of an arm is how you stop the run on that side for good. Running off the end of the top-level array completes the run.
Every step has an id, a slug and a name. The id is a whole number assigned once and never reused — it is what the runs of people currently standing on a step point at, so renumbering one moves people. The slug and name are yours to change freely. Together the id and slug make the step's handle (7-send-pitch) and its step ID (trial_journey.7-send-pitch), which is what appears in the UI, logs and traces.
You may leave id, slug and name out and they will be filled in on save — ids continue from the flow's counter, and the name falls back to the step's type. The slug is derived from the name with the small words dropped ("Send the upgrade pitch" → send-upgrade-pitch`); send one yourself and it is kept as written. Supply them when you want to be sure which step is which across two writes.
Step shapes, the condition vocabulary, and what each step does are all covered in the flows guide.
Validation errors are written to be acted on
Every problem names the step, the bad value, and the fix — so an agent that reads the error can usually repair the definition on its own.
{
"valid": false,
"errors": [\
{\
"path": "nodes[4].email_id",\
"code": "email_unknown",\
"node": "7-send-pitch",\
"severity": "error",\
"message": "7-send-pitch sends email #99, which does not exist. Look up a real one with GET /api/v1/emails."\
}\
],
"warnings": [\
{\
"path": "nodes[3].expires_after",\
"code": "gate_no_expiry",\
"node": "5-wait-for-visit",\
"severity": "warning",\
"message": "5-wait-for-visit has no \"expires_after\", so a person waits here indefinitely until the gate opens and the gate has no paths of its own. Add an \"expires_after\" duration if they should give up after a while, or set it to \"never\" if waiting forever is the intent."\
}\
]
}
Errors block activation; warnings never do. A step that sends a missing email would fail for real people. A gate with no expiry may well be a choice — say so with "expires_after": "never" and the warning goes away.
The checks run in four passes, so a later one can be trusted once the earlier one is clean:
- Shape — every step has an id, a name, a slug and a known type, plus the fields that type requires, and every branch arm is an array of steps.
- Unique ids — an id addresses a step for the whole life of the flow, so no two steps can share one.
- References — every email, sequence, field, product, flow, segment, named duration and predicate exists. An email that's archived is an error. A tag that doesn't exist yet is a warning, since tags are created on demand.
- Dead code — a step sitting after an
exit_flowin the same list is an error, because nobody can ever reach it.
Dry run
curl -X POST https://your-mimeo.com/api/v1/flows/trial_journey/dry_run \
-H "Authorization: Bearer $MIMEO_TOKEN" \
-H "Content-Type: application/json" \
-d '{\
"person": "ada@example.com",\
"event": { "name": "signed_up", "details": { "plan": "trial" } }\
}'
Pass person as an id or an email address. Pass a definition to rehearse something you haven't saved. The response reports whether the trigger matched and then one entry per step, each carrying its step ID, what would happen, and the actual values behind every condition. A branching step also carries arm — the path it sent them down (then/else on an if_else, met/expired on a gate), which is the only way to tell where a run went when that path has no steps on it. It stops at the first place the person would be waiting.
Nothing is written. No run is created, no email is queued, no tag is added. It's a separate read-only walk through the definition, sharing the engine's evaluators so a trace and a real run can't disagree.
Live pages update themselves
A change pushed through this API appears on an open flow page, queue, or sequence page without a refresh. There's no extra call to make — editing the flow is what signals it.