Skip to content
Docs

Webhooks

Events sent to your own server as they happen — an email delivered, bounced or marked as spam, a domain verified — each one signed, so you can tell it came from us.

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

EventSent whenAlso in data
email.sentAmazon SES accepted the email for delivery.—
email.deliveredThe recipient’s mail server accepted it.—
email.delivery_delayedA temporary problem on the way; SES keeps trying.delay.type, when SES names one
email.bouncedThe 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.complainedA recipient reported it as spam. The address goes on your suppression list.complaint.type and complaint.sub_type, when SES gives them
email.failedIt 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.verifiedA domain is verified and ready to send from.—
domain.failedA 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, data holds the email’s email_id (the id POST /emails answered), from, to, subject, tags and created_at (when we accepted it), plus the fields in the table above.
  • For a domain.* event, data holds domain_id, name, status and, for domain.failed, a reason.
  • 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:

HeaderHolds
webhook-idThe message’s id, msg_ and 32 hex digits. The same on every retry and resend of that event to that endpoint.
webhook-timestampWhen this attempt was signed, in whole seconds since 1970 (Unix time).
webhook-signaturev1, 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:

  1. Take the secret after whsec_ and base64-decode it: those bytes are the key.
  2. Join webhook-id, webhook-timestamp and the raw body with dots: id.timestamp.body. Use the body exactly as it arrived — JSON parsed and written out again will not match.
  3. Sign that with HMAC-SHA256 and the key, and base64 the result.
  4. Compare it, in constant time, with each v1, signature in webhook-signature. One match is enough.
  5. Refuse a webhook-timestamp more 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.