> ## Documentation Index
> Fetch the complete documentation index at: https://docs.12m.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Events

> One log per identity. Push or pull, your choice. Same monotonic seq either way.

Every customer-observable thing that happens on an identity — a send,
a received message, a reply on an existing thread, a no-reply timer
firing — lands as an **event** in a per-identity log. The log is
append-only and ordered by a monotonic `seq`.

You can consume the log in two ways:

* **Push** — register a webhook URL. We POST to you on every event,
  signed with HMAC-SHA256.
* **Pull** — call `GET /v1/identities/:handle/events?since=<seq>` and
  walk forward. Optional long-poll up to 25 seconds.

These are not two systems. They are two views of the same log. Each
event has the same `id` and `seq` regardless of which delivery method
you use, so you can mix or migrate freely.

## Event types

The discriminator is always `type`.

| Type                            | Fires when                                                                        |
| ------------------------------- | --------------------------------------------------------------------------------- |
| `email.sent`                    | An outbound left the queue and reached the provider.                              |
| `email.received`                | An inbound message landed in a **new** conversation (first contact).              |
| `email.replied`                 | An inbound message landed in an **existing** conversation (the reply).            |
| `email.no_reply`                | The no-reply timer fired without a reply within the configured window.            |
| `email.bounced`                 | An outbound bounced (mailer-daemon / DSN heuristic). Auto-flips do\_not\_contact. |
| `email.send_failed_permanently` | A queued send was dropped after retries OR on a non-retryable error.              |
| `mailbox.replaced`              | A backing mailbox died; we repinned recipients onto a healthy peer.               |

The set will grow. Bounce events (`email.bounced`) and engagement
events (`email.opened`, `email.clicked`) are on the roadmap. Today,
bounces arrive as inbound messages from `mailer-daemon@…` on the
conversation; you can pattern-match in your handler if you need them.

## Payload

Same shape for every event. The `data` field varies by type.

```json theme={null}
{
  "id":        "evt_01HF6YJZ4N0K3W2X9CPM7B8DG2",
  "seq":       43,
  "type":      "email.replied",
  "createdAt": "2026-04-27T11:55:29.000Z",
  "data": {
    "convId":   "conv_dd6e7130674645d3",
    "identity":    "alice.acme@inboxbase.ai",
    "message": {
      "id":              "<msg-id>",
      "messageIdHeader": "<...@northwindrobotics.com>",
      "from":            "morgan@northwindrobotics.com",
      "to":              "alice.acme@inboxbase.ai",
      "subject":         "Re: Quick intro — fleet rotation",
      "snippet":         "Friday 10 works...",
      "ts":              1777278929000
    }
  }
}
```

## Push or pull?

Pick by environment, not by preference.

<CardGroup cols={2}>
  <Card title="Webhooks" icon="webhook">
    You have a backend with a public URL. You want low latency and
    you're fine running a verifier and an idempotency check on receipt.
    Standard for production sequencers, CRMs, agent runtimes hosted
    on real infra.
  </Card>

  <Card title="Pull (events stream)" icon="rotate">
    You don't want to expose a public URL. You're an agent runtime, an
    MCP server, a desktop tool, a dev environment behind a firewall.
    Long-poll up to 25 seconds; resume from a cursor on restart.
  </Card>
</CardGroup>

You can also use both. The webhook is the primary signal; the events
endpoint is the recovery mechanism if your receiver was offline. Both
carry the same `seq` so dedup is trivial.

## The cursor

`seq` is the integer cursor. It's monotonically increasing per identity
and never reused. The pull endpoint's response shape:

```json theme={null}
{
  "identity":   "alice.acme@inboxbase.ai",
  "events":  [/* events with seq > since */],
  "cursor":  43,
  "hasMore": false
}
```

Pass `cursor` back as the next call's `since`. The same event is
never returned twice on the same connection.

`hasMore: true` means there were more events than your `limit` — loop
without long-polling until you see `hasMore: false`, then resume
long-polling from there.

## Why we built it this way

Two specific choices worth calling out, in case you're evaluating us
against a webhook-only competitor:

**The pull endpoint is a first-class API**, not a fallback. Plenty of
real integrations can't accept a webhook — anything that runs on a
laptop, an MCP server, a low-traffic agent that doesn't justify
standing up a public receiver. We don't want those teams to build a
relay just to listen. They long-poll.

**Push and pull share the cursor.** Some products give you a webhook
without a sequence number; if your receiver crashes mid-batch, you
have to rebuild state by trawling history. With one `seq` across both
delivery modes, recovery is "fetch from `since=<lastSeq>`" — three
lines of code, idempotent, no vendor support ticket required.
