Events · MachineMail Docs

Event types

MachineMail records an event for everything that happens to your mail: a message arrives, a message is sent or bounces, an inbox is created or expires, a domain verifies. Events are stored in a replayable log and delivered three ways:

Channel Use it for
Webhooks Servers that should be called when something happens. Retried, signed, logged.
SSE: GET /v0/events/stream A long-lived process or agent that wants a push stream over plain HTTP.
WebSocket: GET /v0/ws The same stream over a WebSocket, compatible with AgentMail’s protocol.
Event log: GET /v0/events Catching up after downtime, backfill and audit.

All four carry the same events in the same shape.

Event types

Event type When data
message.received An inbound message was accepted into INBOX. message, thread
message.received.quarantined The trust layer quarantined an inbound message. message, thread
message.received.spam An inbound message was classified as spam. message, thread
message.received.unauthenticated An inbound message failed DMARC, or both SPF and DKIM. message, thread
message.received.blocked The sender matched a receive block list. message, thread
message.sent A message was sent from an inbox. send, message, thread
message.delivered The receiving server accepted it. delivery
message.bounced It bounced. bounce
message.complained The recipient marked it as spam. complaint
message.rejected Inbound mail was refused, for example because the inbox is paused. reject
domain.verified A custom domain passed verification. domain
inbox.created An inbox was created, including by catch-all. inbox
inbox.expired An ephemeral inbox expired and was deleted. inbox, deleted_messages
draft.sent A draft was sent, by you or on schedule. draft_id, inbox_id, send
webhook.test You called the webhook test endpoint. Webhook only. webhook_id, message

Each inbound message produces exactly one of the message.received* types. If you only want mail an agent should read, subscribe to message.received alone.

Full payload examples are in the webhook events reference.

The envelope

Webhooks and both streams deliver the same JSON envelope. The event’s data sits under data, and is also spread at the top level for AgentMail compatibility:

message.received

{
  "type": "event",
  "event_id": "evt_01m4fy8hba0000449dcj0gmkc5",
  "event_type": "message.received",
  "timestamp": "2026-10-09T09:00:16.106Z",
  "message": { "message_id": "msg_01m4fy8han0000y3ge3m73vw3w", "...": "..." },
  "thread": { "thread_id": "thd_01m4fy8h340001pp6c74x4688h", "...": "..." },
  "data": {
    "message": {
      "message_id": "msg_01m4fy8han0000y3ge3m73vw3w",
      "inbox_id": "support-agent@machinemail.dev",
      "thread_id": "thd_01m4fy8h340001pp6c74x4688h",
      "labels": ["INBOX", "UNREAD"],
      "from": "ada@example.com",
      "subject": "Re: Hello",
      "extracted_text": "Thanks! Your verification code is 482913.",
      "trust": { "level": "unknown", "score": 0.7, "...": "..." }
    },
    "thread": { "thread_id": "thd_01m4fy8h340001pp6c74x4688h", "message_count": 2, "...": "..." }
  }
}

message in a received event is the full message, including bodies and the trust report, so most handlers need no extra API call.

The event log

Terminal

curl "https://api.machinemail.dev/v0/events?event_types=message.received&limit=20" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"

Response

{
  "count": 1,
  "limit": 20,
  "events": [\
    {\
      "event_id": "evt_01m4fy8hba0000449dcj0gmkc5",\
      "event_type": "message.received",\
      "organization_id": "org_01m4fy8h140000efvagszh6rpx",\
      "pod_id": "pod_01m4fy8h150000w4xty1ehfnqa",\
      "inbox_id": "support-agent@machinemail.dev",\
      "timestamp": "2026-10-09T09:00:16.106Z",\
      "data": { "message": { "...": "..." }, "thread": { "...": "..." } },\
      "created_at": "2026-10-09T09:00:16.106Z"\
    }\
  ]
}
Parameter Notes
after An event_id or an ISO 8601 time. Returns newer events, oldest first. Use the last event_id you processed to resume.
before ISO 8601 time.
event_types Comma-separated or repeated.
inbox_id, pod_id Narrow to one inbox or pod.
limit, page_token, ascending Pagination. Without after, newest first.

GET /v0/events/{event_id} returns one event. The log is also available under /v0/pods/{pod_id}/events and /v0/inboxes/{inbox_id}/events. Reading it needs the metrics_read permission, and lists only the events the key may read (the same per-event rule as the streams below; audit events need metrics_read). An event the key may not read is a 404 on GET /v0/events/{event_id}.

Event IDs sort in creation order, so after=evt_... is a reliable cursor.

Note

The log also holds agent registration audit events: registration.created, token.issued, agent.claimed and agent.revoked. They are not sent to webhooks or streams, and cannot be selected in a webhook’s event_types.

Server-Sent Events

GET /v0/events/stream holds the connection open and writes each event as it happens.

Terminal

curl -N "https://api.machinemail.dev/v0/events/stream?inbox_id=support-agent@machinemail.dev&event_types=message.received" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"

Stream

event: ready
data: {"type":"subscribed"}

id: evt_01m4fy8hba0000449dcj0gmkc5
event: message.received
data: {"type":"event","event_id":"evt_01m4fy8hba0000449dcj0gmkc5","event_type":"message.received",...}

: ping

Which events a key receives

Both streams check every event against the key’s permissions, on top of its scope. An event is sent only when the key holds the read permission of what it describes:

Events Permission needed to receive them
message.* message_read
inbox.* inbox_read
domain.* domain_read
draft.* draft_read
webhook.test webhook_read

Other events are skipped silently, live and on replay, and event_types cannot add them. A metrics_read-only key, for example, opens the SSE stream but receives no mail events. Audit events (agent registrations, members) are in the event log only and never streamed. A dashboard session has every permission, so it receives everything. The key is re-checked every 30 seconds while a stream is open: when it is revoked, or its permissions or scope change, the stream ends with an error. Reconnect to receive what the key allows now.

WebSocket

GET /v0/ws upgrades to a WebSocket that speaks AgentMail’s protocol. Authenticate with the Authorization header or ?token=. The key needs at least one of message_read, inbox_read, domain_read, draft_read or webhook_read (otherwise 403), and receives only the events those permissions allow.

On connect you receive every event in the key’s scope that its permissions allow. Narrow it with a subscribe message:

Client to server

{ "type": "subscribe", "inbox_ids": ["support-agent@machinemail.dev"], "event_types": ["message.received"] }

Server to client

{ "type": "subscribed", "inbox_ids": ["support-agent@machinemail.dev"], "event_types": ["message.received"] }

Events then arrive as the envelope above. Send { "type": "ping" } to get { "type": "pong" }. Invalid messages get { "type": "error", "message": "..." }. Sending subscribe again replaces the previous filter. The server closes connections that stay idle for 2 minutes, so ping periodically.

The WebSocket has no replay. After a reconnect, catch up with GET /v0/events?after=<last event_id>. See Realtime events for a full consumer.

Delivery guarantees

The event log is the source of truth. Webhooks are at least once: the same event can arrive more than once, so deduplicate on event_id. Streams are live only: events that happen while you are disconnected are not queued for you, apart from the SSE replay window.

Metrics

Counts derived from events are at /v0/metrics/events (counts per event type over time), /v0/metrics/usage and /v0/metrics/rates (bounce and complaint rates), with start, end and period parameters. They need metrics_read. See the API reference.