Push: webhooks
Configure one URL per org under Developers → Webhooks. We POST to that URL every time an event lands on any identity in your org. The payload is the standard event shape; the headers carry the signature you verify against your shared secret.- Reject anything older than ~5 minutes. The timestamp
tin the signature is what protects you against replay; without a window check, a leaked old payload is a forever-valid request. - Use a constant-time compare.
timingSafeEqual, not===. - Always read the raw body. If your framework auto-parses JSON,
the signature won’t verify because the bytes shifted under you.
Express
raw(), FastifyaddContentTypeParserwithparseAs: 'buffer', etc.
0s / 5s / 30s after the trigger; per-attempt
timeout 10 seconds. Return any 2xx for success. Return 410 Gone if
the endpoint is dead and you want us to pause delivery (you re-set
from the dashboard).
Pull: the events stream
For environments where you can’t expose a public URL — agents, MCP servers, dev environments behind a firewall, anything running on a laptop — you pull from the same log.cursor back as the next call’s since. The same event is
never returned twice on the same connection.
timeoutMs is the long-poll window. Up to 25 seconds, default 0 (no
long-poll, return immediately whether or not anything was new). With
long-poll on, the call returns immediately once something appends to
the log; if nothing arrives within the window, you get an empty
response and you call again. A typical loop:
Using both
Webhooks for primary delivery, events stream for recovery. If your receiver was down, you missed events. On startup, before resuming normal operation, walk the stream from the lastseq you persisted:
seq, dedup is one line:
if (seenSeqs.has(ev.seq)) continue;
Picking the right granularity
The four event types we emit map cleanly onto the actions a sender typically takes:| Event | Typical action |
|---|---|
email.sent | Update your “in-flight” counter. Persist the convId. |
email.received | A cold inbound (not a reply). Route to a triage queue or agent. |
email.replied | Halt the sequence; route to a human or model. |
email.no_reply | Advance to the next step or mark exhausted. |
Idempotency and ordering
Events are delivered at-least-once via webhook (because retries) and at-most-once per cursor advance via pull. In practice that means:- Dedup by
idorseq, store one of them with whatever side effect your handler did. The next time the same event lands, skip. - Order is monotonic per identity.
seqis strictly increasing. You will never see seq 44 before seq 43 on the same identity. - Order is not monotonic across identities. If you handle multiple
identities in one process, treat each
seqas identity-local.