Set up an endpoint
On the dashboard’s Webhooks page, add the address that should receive events and choose which events it wants. Owners, admins and developers can manage endpoints.
- The address must be
https://on the public internet, on port 443 or 8443 (443 is https’s own: leave the port out). We call no other port, no private or internal address, and follow no redirects. - Each endpoint has its own signing secret, starting
whsec_. It is shown once, when the endpoint is made or its secret is rotated; keep it in your server’s secret settings. Rotating it stops the old secret at once. - You can turn an endpoint off and on, and resend any delivery from its recent list.
The events
| Event | Sent when | Also in data |
|---|---|---|
email.sent | Amazon SES accepted the email for delivery. | — |
email.delivered | The recipient’s mail server accepted it. | — |
email.delivery_delayed | A temporary problem on the way; SES keeps trying. | delay.type, when SES names one |
email.bounced | The recipient’s mail server turned it away. A permanent bounce puts the address on your suppression list. | bounce.type, bounce.sub_type and bounce.diagnostic, when SES gives them |
email.complained | A recipient reported it as spam. The address goes on your suppression list. | complaint.type and complaint.sub_type, when SES gives them |
email.failed | It was not sent: refused when it was due to go, refused by SES, or given up. | reason, the same code as the email’s status_reason |
domain.verified | A domain is verified and ready to send from. | — |
domain.failed | A domain’s records were not found in time, or SES could not set it up. | — |
Events are not sent in a guaranteed order, and one can arrive more than once. Use each event’s created_at to order them, and its webhook-id to act on it once.
The payload
Every event is a JSON object with three fields: type, the event’s name; created_at, when it happened (ISO 8601, UTC); and data.
- For an
email.*event,dataholds the email’semail_id(the idPOST /emailsanswered),from,to,subject,tagsandcreated_at(when we accepted it), plus the fields in the table above. - For a
domain.*event,dataholdsdomain_id,name,statusand, fordomain.failed, areason. - Never the email’s body or attachments: we keep a body only 72 hours, and never send it anywhere else.
The request
Each event is one POST with a JSON body, content-type: application/json, user-agent: MyDomainsHub-Mail-Webhooks/1, and the three headers of Standard Webhooks:
| Header | Holds |
|---|---|
webhook-id | The message’s id, msg_ and 32 hex digits. The same on every retry and resend of that event to that endpoint. |
webhook-timestamp | When this attempt was signed, in whole seconds since 1970 (Unix time). |
webhook-signature | v1, then the base64 HMAC-SHA256 signature. The header can carry several, separated by spaces. |
On the wire, a delivery looks like this:
Check the signature
Check every request before you trust it:
- Take the secret after
whsec_and base64-decode it: those bytes are the key. - Join
webhook-id,webhook-timestampand the raw body with dots:id.timestamp.body. Use the body exactly as it arrived — JSON parsed and written out again will not match. - Sign that with HMAC-SHA256 and the key, and base64 the result.
- Compare it, in constant time, with each
v1,signature inwebhook-signature. One match is enough. - Refuse a
webhook-timestampmore than five minutes from your clock, so a copied request cannot be replayed later.
In Node, with nothing to install:
To test your own check, use the request above with the secret whsec_TXlEb21haW5zSHViIGV4YW1wbGUga2V5 (an example, not a real one). The signed content is the id, the timestamp and the body joined by dots, and the signature it gives is v1,h9l5IZCd5qvgeyFOrxXqaO5bfylqYxNOyrE+DulGTUE=. Your check will refuse it for its age unless you set your clock to its timestamp.
Answers, retries and turning off
- Answer with any 2xx within 10 seconds. Do slow work after you answer. Anything else — a 3xx, 4xx or 5xx, no answer, or a certificate we cannot check — counts as a failure.
- A failed delivery is tried again after 30 seconds, then 2, 8 and 32 minutes, then about 2 hours, then every 6 hours, for 24 hours from when the event happened. After that it is marked failed, and you can still resend it from the dashboard.
- An endpoint that has answered nothing but failures for 3 days, counted from its first failure since its last success, is turned off at its next failure, and what was waiting for it is marked failed. The Webhooks page shows it as off, and the account log records it. Turn it back on there once it is fixed; events that happen while it is off are not kept for it.
- The dashboard shows each delivery’s attempts and the first 500 characters of your answer, so keep secrets out of what you answer.