Skip to main content
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.
TypeFires when
email.sentAn outbound left the queue and reached the provider.
email.receivedAn inbound message landed in a new conversation (first contact).
email.repliedAn inbound message landed in an existing conversation (the reply).
email.no_replyThe no-reply timer fired without a reply within the configured window.
email.bouncedAn outbound bounced (mailer-daemon / DSN heuristic). Auto-flips do_not_contact.
email.send_failed_permanentlyA queued send was dropped after retries OR on a non-retryable error.
mailbox.replacedA 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.
{
  "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.

Webhooks

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.

Pull (events stream)

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.
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:
{
  "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.