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.
/v0/eventsNeeds event_read. Results are limited to your key’s scope: an inbox-scoped key sees only its
inbox’s events.
| Query parameter | Notes |
|---|---|
after | cursor from the previous response; omit it on the first call to start from the oldest retained event |
types | comma-separated event types, for example message.received,thread.locked |
inbox_ids | comma-separated inbox IDs inside your scope |
limit | 1–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.
Cursors
Section titled “Cursors”- 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 withoutafterand reconcile by listing threads. - Delivery is at least once. De-duplicate on
event_id.
A polling loop
Section titled “A polling loop”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.
Inbox events
Section titled “Inbox events”/v0/inboxes/{inbox_id}/eventslists 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.