Events
Replayable event log and live streams.
| Method | Path | Summary |
|---|---|---|
GET | /v0/events | List Events |
GET | /v0/events/stream | Stream Events (SSE) |
GET | /v0/events/{event_id} | Get Event |
GET | /v0/inboxes/{inbox_id}/events | List Events in Inbox |
GET | /v0/inboxes/{inbox_id}/events/{event_id} | Get Event in Inbox |
GET | /v0/pods/{pod_id}/events | List Events in Pod |
GET | /v0/pods/{pod_id}/events/{event_id} | Get Event in Pod |
GET | /v0/ws | WebSocket Event Stream |
List Events
The replayable event log, newest first. Pass after (an event ID or a time) to read forward from a point, oldest first. Events are kept without a retention limit. Only the events the credential may read are listed, as on the live streams: message.* needs message_read, inbox.* inbox_read, domain.* domain_read, draft.* draft_read, webhook.test webhook_read, and the audit events (agent registrations, members) metrics_read. event_types cannot add others.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: metrics_read.
Query parameters
limitintegerpage_tokenstringascendingbooleanafterstringbeforestring (date-time)event_types("message.received" | "message.received.spam" | "message.received.quarantined" | "message.received.unauthenticated" | "message.received.blocked" | "message.sent" | "message.delivered" | "message.bounced" | "message.complained" | "message.rejected" | "domain.verified" | "domain.updated" | "inbox.created" | "inbox.expired" | "draft.sent" | "webhook.test")[]inbox_idstringpod_idstringResponse
200 A page of events (application/json)
countintegerRequiredlimitintegernext_page_tokenstringEach item of events is a Event:
event_idstringRequiredevent_type"message.received" | "message.received.spam" | "message.received.quarantined" | "message.received.unauthenticated" | "message.received.blocked" | "message.sent" | "message.delivered" | "message.bounced" | "message.complained" | "message.rejected" | "domain.verified" | "domain.updated" | "inbox.created" | "inbox.expired" | "draft.sent" | "webhook.test" | "registration.created" | "token.issued" | "agent.claimed" | "agent.revoked" | "member.invited" | "member.role_changed" | "member.removed"Requiredorganization_idstringRequiredpod_idstringinbox_idstringdataRecord<string, unknown>Requiredtimestampstring (date-time)Requiredcreated_atstring (date-time)RequiredErrors
| Status | Meaning |
|---|---|
400 | Invalid request: error.type is validation and error.param names the field. |
401 | Missing, invalid, expired or revoked credential. |
403 | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account_suspended: error.message says what still works; do not retry or re-register). |
500 | Unexpected server error. |
Error bodies use the error envelope.
Example
Stream Events (SSE)
Server-Sent Events of everything in the credential scope that its permissions allow, narrowed by the query filters.
Each event is sent only when the credential holds the read permission of what it describes: message.* needs message_read, domain.* needs domain_read, inbox.* needs inbox_read, draft.* needs draft_read, webhook.* needs webhook_read. Other events are skipped, without an error, and event_types cannot add them. The credential is re-checked every 30 seconds: when it is revoked or expired, or its permissions or pod/inbox scope change, the stream ends with an error (reconnect to get the events the credential allows now).
Frames are id: <event_id>, event: <event_type>, data: <JSON>, where the JSON is the webhook event envelope (type: "event", event_id, event_type, timestamp, data, and the data keys repeated at the top level), not the Event object of GET /v0/events. The first frame is event: ready with data: {"type":"subscribed"}. A : ping comment is sent after 15 seconds without events.
With a Last-Event-ID: evt_... header, up to 500 missed events are replayed first. The replay applies event_types, the credential scope and the per-event permissions, not the inbox_id or pod_id filters. When the API runs on several servers, the replay also repeats events from the few seconds before Last-Event-ID (an event from another server can reach a stream after a later one): dedupe by event id.
An event: resync frame (data: {"type":"resync"}) means this server may have missed events from other servers (its notification connection to the database was interrupted); the stream then ends. Its id: is a cursor from before the interruption, which an EventSource takes as its last event ID: reconnect with it as Last-Event-ID to replay what was missed, some events again (dedupe by event id).
A stream opened with a dashboard session cookie also receives that person’s session events (session.agent.claimed, session.cli.device, session.oauth.request): live notices that a hand-off they started (an agent claim, a CLI sign-in, an OAuth consent) was completed, so their other tabs can finish too. Their frames have no id: line and their data is {"type":"session","event_type","timestamp","data"}; they are never stored, replayed or sent to API keys, OAuth tokens, webhooks or the WebSocket. They need no permission: they go only to the session of the person they are about.
Authentication: Authorization: Bearer <api key> or ?token=<api key> or the dashboard mm_session cookie. Permission: metrics_read.
Query parameters
inbox_idstring[]inbox_idsstring[]pod_idstring[]pod_idsstring[]event_types("message.received" | "message.received.spam" | "message.received.quarantined" | "message.received.unauthenticated" | "message.received.blocked" | "message.sent" | "message.delivered" | "message.bounced" | "message.complained" | "message.rejected" | "domain.verified" | "domain.updated" | "inbox.created" | "inbox.expired" | "draft.sent" | "webhook.test")[]Headers
Last-Event-IdstringResponse
200 The event stream (text/event-stream)
Errors
| Status | Meaning |
|---|---|
400 | Invalid request: error.type is validation and error.param names the field. |
401 | Missing, invalid, expired or revoked credential. |
403 | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account_suspended: error.message says what still works; do not retry or re-register). |
500 | Unexpected server error. |
Error bodies use the error envelope.
Example
Get Event
One event of the log. An event the credential may not read (see GET /v0/events) is a 404, like one outside its scope.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: metrics_read.
Path parameters
event_idstringRequiredResponse
200 The event (application/json)
event_idstringRequiredevent_type"message.received" | "message.received.spam" | "message.received.quarantined" | "message.received.unauthenticated" | "message.received.blocked" | "message.sent" | "message.delivered" | "message.bounced" | "message.complained" | "message.rejected" | "domain.verified" | "domain.updated" | "inbox.created" | "inbox.expired" | "draft.sent" | "webhook.test" | "registration.created" | "token.issued" | "agent.claimed" | "agent.revoked" | "member.invited" | "member.role_changed" | "member.removed"Requiredorganization_idstringRequiredpod_idstringinbox_idstringdataRecord<string, unknown>Requiredtimestampstring (date-time)Requiredcreated_atstring (date-time)RequiredErrors
| Status | Meaning |
|---|---|
401 | Missing, invalid, expired or revoked credential. |
403 | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account_suspended: error.message says what still works; do not retry or re-register). |
404 | Not found. Also returned for resources outside the credential scope. |
500 | Unexpected server error. |
Error bodies use the error envelope.
Example
List Events in Inbox
Same as List Events (GET /v0/events), limited to the inbox.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: metrics_read.
Path parameters
inbox_idstringRequiredQuery parameters
limitintegerpage_tokenstringascendingbooleanafterstringbeforestring (date-time)event_types("message.received" | "message.received.spam" | "message.received.quarantined" | "message.received.unauthenticated" | "message.received.blocked" | "message.sent" | "message.delivered" | "message.bounced" | "message.complained" | "message.rejected" | "domain.verified" | "domain.updated" | "inbox.created" | "inbox.expired" | "draft.sent" | "webhook.test")[]inbox_idstringpod_idstringResponse
200 A page of events (application/json)
countintegerRequiredlimitintegernext_page_tokenstringEach item of events is a Event:
event_idstringRequiredevent_type"message.received" | "message.received.spam" | "message.received.quarantined" | "message.received.unauthenticated" | "message.received.blocked" | "message.sent" | "message.delivered" | "message.bounced" | "message.complained" | "message.rejected" | "domain.verified" | "domain.updated" | "inbox.created" | "inbox.expired" | "draft.sent" | "webhook.test" | "registration.created" | "token.issued" | "agent.claimed" | "agent.revoked" | "member.invited" | "member.role_changed" | "member.removed"Requiredorganization_idstringRequiredpod_idstringinbox_idstringdataRecord<string, unknown>Requiredtimestampstring (date-time)Requiredcreated_atstring (date-time)RequiredErrors
| Status | Meaning |
|---|---|
400 | Invalid request: error.type is validation and error.param names the field. |
401 | Missing, invalid, expired or revoked credential. |
403 | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account_suspended: error.message says what still works; do not retry or re-register). |
404 | Not found. Also returned for resources outside the credential scope. |
500 | Unexpected server error. |
Error bodies use the error envelope.
Example
Get Event in Inbox
Same as Get Event (GET /v0/events/{event_id}), limited to the inbox.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: metrics_read.
Path parameters
inbox_idstringRequiredevent_idstringRequiredResponse
200 The event (application/json)
event_idstringRequiredevent_type"message.received" | "message.received.spam" | "message.received.quarantined" | "message.received.unauthenticated" | "message.received.blocked" | "message.sent" | "message.delivered" | "message.bounced" | "message.complained" | "message.rejected" | "domain.verified" | "domain.updated" | "inbox.created" | "inbox.expired" | "draft.sent" | "webhook.test" | "registration.created" | "token.issued" | "agent.claimed" | "agent.revoked" | "member.invited" | "member.role_changed" | "member.removed"Requiredorganization_idstringRequiredpod_idstringinbox_idstringdataRecord<string, unknown>Requiredtimestampstring (date-time)Requiredcreated_atstring (date-time)RequiredErrors
| Status | Meaning |
|---|---|
401 | Missing, invalid, expired or revoked credential. |
403 | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account_suspended: error.message says what still works; do not retry or re-register). |
404 | Not found. Also returned for resources outside the credential scope. |
500 | Unexpected server error. |
Error bodies use the error envelope.
Example
List Events in Pod
Same as List Events (GET /v0/events), limited to the pod.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: metrics_read.
Path parameters
pod_idstringRequiredQuery parameters
limitintegerpage_tokenstringascendingbooleanafterstringbeforestring (date-time)event_types("message.received" | "message.received.spam" | "message.received.quarantined" | "message.received.unauthenticated" | "message.received.blocked" | "message.sent" | "message.delivered" | "message.bounced" | "message.complained" | "message.rejected" | "domain.verified" | "domain.updated" | "inbox.created" | "inbox.expired" | "draft.sent" | "webhook.test")[]inbox_idstringpod_idstringResponse
200 A page of events (application/json)
countintegerRequiredlimitintegernext_page_tokenstringEach item of events is a Event:
event_idstringRequiredevent_type"message.received" | "message.received.spam" | "message.received.quarantined" | "message.received.unauthenticated" | "message.received.blocked" | "message.sent" | "message.delivered" | "message.bounced" | "message.complained" | "message.rejected" | "domain.verified" | "domain.updated" | "inbox.created" | "inbox.expired" | "draft.sent" | "webhook.test" | "registration.created" | "token.issued" | "agent.claimed" | "agent.revoked" | "member.invited" | "member.role_changed" | "member.removed"Requiredorganization_idstringRequiredpod_idstringinbox_idstringdataRecord<string, unknown>Requiredtimestampstring (date-time)Requiredcreated_atstring (date-time)RequiredErrors
| Status | Meaning |
|---|---|
400 | Invalid request: error.type is validation and error.param names the field. |
401 | Missing, invalid, expired or revoked credential. |
403 | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account_suspended: error.message says what still works; do not retry or re-register). |
404 | Not found. Also returned for resources outside the credential scope. |
500 | Unexpected server error. |
Error bodies use the error envelope.
Example
Get Event in Pod
Same as Get Event (GET /v0/events/{event_id}), limited to the pod.
Authentication: Authorization: Bearer <api key> or the dashboard mm_session cookie. Permission: metrics_read.
Path parameters
pod_idstringRequiredevent_idstringRequiredResponse
200 The event (application/json)
event_idstringRequiredevent_type"message.received" | "message.received.spam" | "message.received.quarantined" | "message.received.unauthenticated" | "message.received.blocked" | "message.sent" | "message.delivered" | "message.bounced" | "message.complained" | "message.rejected" | "domain.verified" | "domain.updated" | "inbox.created" | "inbox.expired" | "draft.sent" | "webhook.test" | "registration.created" | "token.issued" | "agent.claimed" | "agent.revoked" | "member.invited" | "member.role_changed" | "member.removed"Requiredorganization_idstringRequiredpod_idstringinbox_idstringdataRecord<string, unknown>Requiredtimestampstring (date-time)Requiredcreated_atstring (date-time)RequiredErrors
| Status | Meaning |
|---|---|
401 | Missing, invalid, expired or revoked credential. |
403 | The credential lacks the permission or scope for this request (error.type forbidden), or MachineMail suspended the organization or its sending (account_suspended: error.message says what still works; do not retry or re-register). |
404 | Not found. Also returned for resources outside the credential scope. |
500 | Unexpected server error. |
Error bodies use the error envelope.
Example
WebSocket Event Stream
Upgrades to a WebSocket (send Connection: Upgrade and Upgrade: websocket). The socket starts subscribed to every event in the credential scope; query parameters are not read.
Client messages are JSON: {"type":"subscribe","inbox_ids":[...],"pod_ids":[...],"event_types":[...]} replaces the subscription (the server answers {"type":"subscribed","inbox_ids","pod_ids","event_types"}), {"type":"ping"} is answered with {"type":"pong"}, anything else with {"type":"error","message"}. Event messages are the webhook event envelope (type: "event", event_id, event_type, timestamp, data, and the data keys repeated at the top level), not the Event object of GET /v0/events.
Each event is sent only when the credential holds the read permission of what it describes: message.* needs message_read, domain.* needs domain_read, inbox.* needs inbox_read, draft.* needs draft_read, webhook.* needs webhook_read. Other events are skipped, without an error, and event_types cannot add them. The credential is re-checked every 30 seconds: when it is revoked or expired, or its permissions or pod/inbox scope change, the stream ends with an error (reconnect to get the events the credential allows now).
Opening the socket needs at least one of those permissions; a credential with none is refused with 403. A request without the upgrade headers is a 404.
The server closes the socket with code 1012 when it may have missed events from other servers (its notification connection to the database was interrupted): reconnect and refetch, since the WebSocket has no replay.
Authentication: Authorization: Bearer <api key> or ?token=<api key> or the dashboard mm_session cookie. Permission: metrics_read or message_read.
Response
101 Switching Protocols: the connection is now a WebSocket.
Errors
| Status | Meaning |
|---|---|
400 | The WebSocket handshake failed (plain text WebSocket upgrade failed). |
401 | Missing, invalid, expired or revoked credential. |
403 | The credential holds no permission the stream serves events for (plain text Forbidden). |
404 | Not found. Also returned for resources outside the credential scope. |
500 | Unexpected server error. |
Error bodies use the error envelope.