Verify webhooks
Check the X-MachineMail-Signature header on every webhook delivery in TypeScript, Python and Go, reject replays, deduplicate retries, and test or replay deliveries.
MachineMail signs every webhook delivery with your webhook’s secret. Verify the signature before you trust anything in the body. Without it, anyone who finds your endpoint URL can post a fake message.received and steer your agent.
What a delivery looks like
Each delivery is an HTTPS POST with a JSON body and these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | MachineMail-Webhooks/1.0 |
X-MachineMail-Signature | t=<unix seconds>,v1=<hex signature> |
X-MachineMail-Event | The event type, for example message.received |
X-MachineMail-Event-Id | The event ID, for example evt_01m4fy8hba0000449dcj0gmkc5 |
X-MachineMail-Delivery-Id | The delivery ID, for example dlv_01m4fy8hk70000a2b4c6d8e0f1 |
The body has the event envelope. The event’s data is in data, and its keys are also repeated at the top level for AgentMail compatibility:
See Webhook events for the payload of each event type.
How the signature works
- The key is the whole secret string as returned by the API, including the
whsec_prefix. It is in thesecretfield ofPOST /v0/webhooksandGET /v0/webhooks/{webhook_id}. - The message is the timestamp, a dot, then the exact bytes of the request body. Parse the JSON only after the check passes; re-serializing it changes the bytes.
- Compare in constant time.
- Reject timestamps more than 300 seconds from your clock. That stops an attacker from replaying a delivery they captured.
Verify in your language
With FastAPI, read the raw body with await request.body() and pass the bytes to the same function.
Warning
Frameworks that parse JSON before your handler runs (Express with express.json(), for example) hand you an object, not the original bytes. Use a raw body parser on the webhook route.
Respond fast and deduplicate
- Return any 2xx status within 10 seconds. Anything else, a timeout or a connection error counts as a failure.
- Redirects are not followed. Point the webhook at the final URL.
- Delivery is at least once. A delivery can arrive twice, for example after a network error on our side or a replay. Deduplicate on
X-MachineMail-Event-Id(the same event to the same endpoint) and do the work idempotently. - Do slow work, such as calling a model, after you respond. Put the event on a queue.
Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 12 hours and then every 24 hours, until 5 days after the event. After that the delivery is marked failed. See Webhooks.
Test, inspect and replay
Send a synthetic webhook.test event to an endpoint and get the delivery result back:
The test event’s data is { "webhook_id": "...", "message": "This is a test event from MachineMail." }. It is signed like any other delivery, so it exercises your verification code.
List recent deliveries, optionally filtered by status (pending, delivered or failed):
Each delivery records attempt, response_status, the first 1 KB of response_body, last_error and next_attempt_at. After you fix your endpoint, replay a delivery. It creates a new delivery of the same event and attempts it immediately:
A replay has a new X-MachineMail-Delivery-Id but the same X-MachineMail-Event-Id, so your deduplication still works. Testing and replaying need the webhook_update permission. The dashboard shows the same log with a replay button at /app/webhooks.
Rotate a secret
There is no rotate endpoint. Create a second webhook for the same URL, accept both secrets while you switch over, then delete the old webhook.