# Event contract

Events are Mimeo's trigger stream: a name, a person and whatever details you carry. The shape, the events Mimeo writes itself, and how to design your own.

Events are the trigger stream — the one way the outside world tells Mimeo that something happened. Event-triggered flows start here; native [renewal-approaching flows](https://mimeohq.com/docs/usage/flows#renewal-approaching) start from subscription dates instead, without a new event type.

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. One name is reserved: `internal_note` is refused here — internal notes go through [the notes endpoint](https://mimeohq.com/docs/api/people#notes), which never triggers automation. |
| `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](https://mimeohq.com/docs/usage/people) 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](https://mimeohq.com/docs/usage/events), over [`GET /api/v1/event_types`](https://mimeohq.com/docs/api/events#event-types), 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`](https://mimeohq.com/docs/api/events#reading-events).

## 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](https://mimeohq.com/docs/api/events) · [Flow schema](https://mimeohq.com/docs/reference/flow-schema)
