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.
id and seq regardless of which delivery method
you use, so you can mix or migrate freely.
Event types
The discriminator is alwaystype.
| 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. |
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. Thedata field varies by type.
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.
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:
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 oneseq across both
delivery modes, recovery is “fetch from since=<lastSeq>” — three
lines of code, idempotent, no vendor support ticket required.