# Event contract

Events are the trigger stream — the one way the outside world tells Mimeo that
something happened. Everything the automation engine does starts here.

An event is a **name**, a **person**, and whatever **details** you want to carry. There is no schema and there never will be: your app's vocabulary is your data, not our product vocabulary.

```
POST /api/v1/events
Authorization: Bearer mm_your_token
Content-Type: application/json

{
  "email": "ada@example.com",
  "name": "signed_up",
  "details": { "plan": "trial", "source": "ads" },
  "occurred_at": "2026-07-29T10:00:00Z"
}
```

## The fields

| Field              | Notes                                                                                          |
|--------------------|------------------------------------------------------------------------------------------------|  
| `email`            | Required. The universal key — lowercased, and the person is created if they're new.          |
| `name`             | Required. Your own vocabulary. Triggers match on it exactly.                                  |
| `details`          | Any JSON object. Trigger matches and `event_occurred` conditions read these.                  |
| `occurred_at`     | Optional. Defaults to now. Timeline ordering.                                                 |
| `idempotency_key`  | Optional. A repeat of the same key is recognised as a duplicate and does nothing, which makes an at-least-once sender safe. |
| `person`           | Optional. `first_name`, `last_name`, `fields`, `tags` — upserted alongside the event.        |
| `attribution`      | Optional, first touch only: landing page, referrer, UTMs, device, country.                   |

## Batching

Send up to 100 at once as `{ "events": [ … ] }`. The response carries a
per-event result, so a partial failure tells you exactly which ones.

```
{ "results": [\
    { "status": "created",   "person_id": 41, "event_id": 902 },\
    { "status": "duplicate", "person_id": 41, "event_id": 887 },\
    { "status": "invalid",   "error": "email is required" } ] }
```

## Events Mimeo writes itself

Your events aren't the only ones in the stream. State changes fire events too, so
a flow can trigger on them exactly as it would on yours:

| Event                        | When                                                                                                          |
|------------------------------|----------------------------------------------------------------------------------------------------------------|
| `tag_added` · `tag_removed`  | A tag changes, by hand or by a flow. Details carry the tag.                                                  |
| `field_changed`              | A custom field changes. Details carry the key, the old value and the new one.                                 |
| `unsubscribed` · `resubscribed` | Suppression state changes. Details carry the reason and how it happened.                                     |
| `sequence_completed`         | Someone reaches the end of a sequence.                                                                        |

A flow's `fire_event` step writes one too — which is how one flow hands
off to another. The validator warns (`self_trigger`) if a flow fires
the event that triggers itself.

## Money is events

Purchases, subscriptions and payments are recorded from events with the right
names and details — Mimeo never integrates a payment processor directly, because
your systems already know and one source of truth beats two. See
[People](/content/docs/usage/people.html) for the shapes.

## What events are not

**Opens and clicks are not events.** They're telemetry, in their own
tables, with bot filtering — deliberately not part of the trigger stream, because
a flow branching on "opened" is a flow branching on a number that Apple Mail
Privacy Protection inflates. Use the `opened_email` and
`clicked_email` conditions if you want them, knowing that.

**Tags don't decide.** Tags describe. Anything a flow branches on
should be real state — a field, a purchase, a subscription, an event that
actually happened.

## Every name registers itself

The first time a name arrives — from your systems, from the engine, or
from an import — it registers itself in the **event** **registry**: one row per name, carrying its occurrence count,
sources, first/last seen, and a merged _shape_ of every payload
ever sent under it. Nothing is declared ahead of time, so the registry
is exactly what has actually happened — the live vocabulary triggers,
gates and `event_occurred` conditions draw from. Read it in
the UI under [Events](/content/docs/usage/events.html), over
[`GET /api/v1/event_types`](/content/docs/api/events.html#event-types/index.html),
or with the MCP `list_event_types` tool — and
`describe_schema` returns the names as
`event_names`. The feed of individual occurrences is
readable too:
[`GET /api/v1/events`](/content/docs/api/events.html#reading-events/index.html).

## Designing your event names

- Name what happened, in the past tense, from the person's side: `signed_up`, `started_trial`, `viewed_pricing`.
- Prefer one event with details over many near-identical names. `plan_changed` with `{ from, to }` beats `upgraded_to_pro`, `upgraded_to_team` and the rest — a trigger match can narrow it, and you don't have to touch a flow when you add a plan.
- Send an `idempotency_key` if your sender might retry. It's the cheapest insurance there is.

See also: [Events API](/content/docs/api/events.html) · [Flow schema](/content/docs/reference/flow-schema.html)
