Skip to content
machinemail

Webhooks

MachineMail webhooks POST signed events to your HTTPS endpoint, filtered by event type, pod or inbox, with retries for five days and a replayable delivery log.

A webhook is an HTTPS endpoint that MachineMail calls when an event happens. Each call is a POST with the event envelope as JSON, signed with a secret only you and MachineMail know.

Creating a webhook

Terminal
curl https://api.machinemail.dev/v0/webhooks \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/machinemail",
    "event_types": ["message.received", "message.received.quarantined"],
    "inbox_ids": ["support-agent@machinemail.dev"],
    "client_id": "support-hook"
  }'
create-webhook.ts
const res = await fetch('https://api.machinemail.dev/v0/webhooks', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.MACHINEMAIL_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    url: 'https://example.com/hooks/machinemail',
    event_types: ['message.received', 'message.received.quarantined'],
    inbox_ids: ['support-agent@machinemail.dev'],
    client_id: 'support-hook',
  }),
});
const webhook = await res.json();
// Store webhook.secret; you need it to verify deliveries.
create_webhook.py
import os, requests

webhook = requests.post(
    "https://api.machinemail.dev/v0/webhooks",
    headers={"Authorization": f"Bearer {os.environ['MACHINEMAIL_API_KEY']}"},
    json={
        "url": "https://example.com/hooks/machinemail",
        "event_types": ["message.received", "message.received.quarantined"],
        "inbox_ids": ["support-agent@machinemail.dev"],
        "client_id": "support-hook",
    },
).json()
Response
{
  "webhook_id": "whk_01m4fy8heb0000jbck9me3gqxb",
  "url": "https://example.com/hooks/machinemail",
  "event_types": ["message.received", "message.received.quarantined"],
  "inbox_ids": ["support-agent@machinemail.dev"],
  "secret": "whsec_hbANq3v94BDGbt1lDkzB8Y0tXJNVOSo9",
  "enabled": true,
  "status": "active",
  "client_id": "support-hook",
  "created_at": "2026-10-09T09:00:16.203Z",
  "updated_at": "2026-10-09T09:00:16.203Z"
}
FieldNotes
urlRequired. http or https to a public address. Private and internal addresses are refused.
event_typesEvent types to send. Omit it or pass an empty list to receive every type. Unknown types are a validation error.
inbox_idsOnly events for these inboxes.
pod_idsOnly events for these pods.
client_idIdempotency key.

You can also create webhooks in the dashboard at machinemail.dev/app/webhooks.

Filters and scope

An event is delivered to a webhook when all of these hold:

  • the webhook is enabled,
  • event_types is empty or contains the event’s type,
  • pod_ids is empty or contains the event’s pod,
  • inbox_ids is empty or contains the event’s inbox.

A webhook never delivers an event its creator could not read. Creating one needs the read permission of every listed event type (message.* needs message_read, inbox.* inbox_read, domain.* domain_read, draft.* draft_read, webhook.test webhook_read), otherwise 403. An empty event_types from a key without all of them is stored as the types that key may read. Adding event types or changing the url of an existing webhook follows the same rule.

A webhook created with a pod-scoped key, or under /v0/pods/{pod_id}/webhooks, is bound to that pod (its pod_id is set) and only receives that pod’s events. The same goes for inbox keys and /v0/inboxes/{inbox_id}/webhooks. Events without an inbox, such as domain.verified, never match a webhook filtered by inbox_ids.

The request

Request
POST /hooks/machinemail HTTP/1.1
Content-Type: application/json
User-Agent: MachineMail-Webhooks/1.0
X-MachineMail-Signature: t=1791536416,v1=5f2c...e81a
X-MachineMail-Event: message.received
X-MachineMail-Event-Id: evt_01m4fy8hba0000449dcj0gmkc5
X-MachineMail-Delivery-Id: dlv_01m4fy8hbc0000ywxk3s1bkzf4

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

Respond with any 2xx status within 10 seconds. Do slow work after you respond.

Signatures

X-MachineMail-Signature is t=<unix seconds>,v1=<hex HMAC-SHA256>. The HMAC is computed with the webhook’s secret (the full string, including whsec_) over the timestamp, a dot and the raw request body:

text
v1 = hex(hmac_sha256(secret, t + "." + raw_body))

To verify, recompute it from the raw body bytes (not re-serialized JSON), compare in constant time, and reject timestamps more than 5 minutes from your clock to stop replays. Code for TypeScript, Python and Go is in Verify webhooks.

The secret is returned on the webhook object (GET /v0/webhooks/{webhook_id}), so keep keys with webhook_read private.

Retries

A delivery fails on a non-2xx response, a timeout or a connection error. Redirects are not followed and count as failures. Failed deliveries are retried after:

AttemptDelay after the previous failure
21 minute
35 minutes
430 minutes
52 hours
612 hours
7 and later24 hours

Retries stop five days after the event; the delivery is then marked failed. Delivery is at least once. The same event can arrive twice, for example if your server processed it but the response was lost, so deduplicate on X-MachineMail-Event-Id (the same value as event_id in the body).

Delivery log, replay and test

Every attempt is recorded.

Terminal
curl "https://api.machinemail.dev/v0/webhooks/whk_01m4fy8heb0000jbck9me3gqxb/deliveries?status=failed" \
  -H "Authorization: Bearer $MACHINEMAIL_API_KEY"
Delivery
{
  "delivery_id": "dlv_01m4fy8hbc0000ywxk3s1bkzf4",
  "webhook_id": "whk_01m4fy8heb0000jbck9me3gqxb",
  "event_id": "evt_01m4fy8hba0000449dcj0gmkc5",
  "event_type": "message.received",
  "attempt": 3,
  "status": "pending",
  "response_status": 503,
  "response_body": "upstream unavailable",
  "next_attempt_at": "2026-10-09T09:36:16.000Z",
  "last_error": "HTTP 503",
  "created_at": "2026-10-09T09:00:16.106Z",
  "updated_at": "2026-10-09T09:06:16.000Z"
}

status is pending (waiting for its next attempt), delivered or failed. response_body keeps the first 1 KB of your response.

MethodPathNotes
GET/v0/webhooks/{webhook_id}/deliveriesFilter with status. Paginated.
POST/v0/webhooks/{webhook_id}/deliveries/{delivery_id}/replaySends the same event again now, as a new delivery.
POST/v0/webhooks/{webhook_id}/testSends a webhook.test event now and returns the delivery.

Replay and test need webhook_update. The dashboard shows the same log with a replay button on each webhook’s page.

Updating and disabling

PATCH /v0/webhooks/{webhook_id} accepts:

FieldNotes
urlNew endpoint.
event_typesReplaces the list.
add_inbox_ids, remove_inbox_idsEdit the inbox filter.
add_pod_ids, remove_pod_idsEdit the pod filter.
enabledfalse stops deliveries (status becomes disabled); true resumes them.

Events that happen while a webhook is disabled are not queued for it. Use the event log to catch up.

DELETE /v0/webhooks/{webhook_id} removes it. Pending deliveries for a deleted webhook fail.

Note

Custom request headers on webhooks (AgentMail’s /headers endpoints) are not yet available.

Next