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
| Field | Notes |
|---|---|
url | Required. http or https to a public address. Private and internal addresses are refused. |
event_types | Event types to send. Omit it or pass an empty list to receive every type. Unknown types are a validation error. |
inbox_ids | Only events for these inboxes. |
pod_ids | Only events for these pods. |
client_id | Idempotency 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_typesis empty or contains the event’s type,pod_idsis empty or contains the event’s pod,inbox_idsis 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
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:
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:
| Attempt | Delay after the previous failure |
|---|---|
| 2 | 1 minute |
| 3 | 5 minutes |
| 4 | 30 minutes |
| 5 | 2 hours |
| 6 | 12 hours |
| 7 and later | 24 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.
status is pending (waiting for its next attempt), delivered or failed. response_body keeps the first 1 KB of your response.
| Method | Path | Notes |
|---|---|---|
GET | /v0/webhooks/{webhook_id}/deliveries | Filter with status. Paginated. |
POST | /v0/webhooks/{webhook_id}/deliveries/{delivery_id}/replay | Sends the same event again now, as a new delivery. |
POST | /v0/webhooks/{webhook_id}/test | Sends 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:
| Field | Notes |
|---|---|
url | New endpoint. |
event_types | Replaces the list. |
add_inbox_ids, remove_inbox_ids | Edit the inbox filter. |
add_pod_ids, remove_pod_ids | Edit the pod filter. |
enabled | false 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
/headers endpoints) are not yet available.