Skip to content
simailDocsv0

Integrate

Polling events

Read every event with GET /v0/events and a cursor; no public endpoint needed. Cursors, filters, retention and a polling loop.

docs.simail.ai/events

GET /v0/events is the simplest way to react to mail: no public URL, no signature checks, and nothing is missed while your agent is offline (events are kept for 30 days). It returns the same thin payloads as webhooks and the WebSocket; see Event types.

GET/v0/events

Needs event_read. Results are limited to your key’s scope: an inbox-scoped key sees only its inbox’s events.

Query parameterNotes
aftercursor from the previous response; omit it on the first call to start from the oldest retained event
typescomma-separated event types, for example message.received,thread.locked
inbox_idscomma-separated inbox IDs inside your scope
limit1–200, default 50
curl -s "https://api.simail.ai/v0/events?types=message.received&limit=100" \
-H "Authorization: Bearer $SIMAIL_API_KEY"
{
"count": 1,
"limit": 100,
"events": [
{ "event_id": "evt_…", "event_type": "message.received", "occurred_at": "2026-10-18T17:02:12Z", "organization_id": "org_…", "inbox_id": "shop-helper@simail.email", "thread_id": "thr_…", "message_id": "msg_…", "data": { "from": "ada@example.com", "labels": ["received", "unread"], "held": false, "classification": "normal", "wake": true } }
],
"next_cursor": "c_…"
}

Store next_cursor and pass it as after next time. Events come in the order they were recorded. Polling returns every event, including ones that did not wake your agent (data.wake: false); filter on it if you only want waking events.

  • A cursor is opaque and signed. It is bound to your organization, your key’s scope and the filters you used; using it with another key or other filters returns 400 invalid_request.
  • Events are kept for 30 days. A cursor older than that returns 410 gone: start again without after and reconcile by listing threads.
  • Delivery is at least once. De-duplicate on event_id.
let cursor: string | undefined = await loadCursor();
for (;;) {
const url = new URL("https://api.simail.ai/v0/events");
url.searchParams.set("types", "message.received,thread.locked,thread.unlocked");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("after", cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.SIMAIL_API_KEY}` } });
if (res.status === 429) { await sleep(Number(res.headers.get("retry-after") ?? 5) * 1000); continue; }
const page = await res.json();
for (const event of page.events) await handle(event); // idempotent on event.event_id
if (page.next_cursor) { cursor = page.next_cursor; await saveCursor(cursor); }
if (page.count < 100) await sleep(5000);
}

Handle each event before saving the cursor, so a crash replays events instead of losing them.

GET/v0/inboxes/{inbox_id}/events

lists the events of one inbox with the same parameters (except inbox_ids). Needs event_read.

The MCP tool list_events takes the same after, types, inbox_ids and limit.