Skip to main content
The API so far is something you call. Webhooks are the other direction: Ruber calls you, as things happen, so an integration does not have to poll a mailbox to find out a message arrived.

Events

The last four are in the catalogue so you can subscribe to them now without a breaking change later; nothing raises them yet, and this page will say so until something does.
Private mailboxes never raise message.received.Not a limitation to lift later. The event payload carries a subject and a sender, and a payload is a row in our database — so emitting one for a Private mailbox would put exactly the plaintext we promise never to hold at rest into storage we can read.A stripped payload with no headers was the obvious compromise and is also wrong: it would leak who is corresponding and how often, and the event would be useless anyway, since every API endpoint refuses to read a Private mailbox.

Registering an endpoint

Settings → Webhooks in the dashboard. Give it an HTTPS URL and tick the events you want. You get a signing secret beginning whsec_, shown once. Unlike an API key it is stored intact — we need it to sign — but it is still only displayed at creation. Your URL must be HTTPS and must resolve to a public address. Endpoints resolving to loopback, private ranges, or cloud metadata addresses are refused on every attempt, checked at delivery time rather than only at registration, so a name repointed later is caught too.

The envelope

Every delivery is a POST with this body:
id is the delivery id. It is the same across every retry of that delivery, which is what makes it usable for deduplication — see below.
from and envelope_from are different things and both are given. from is the header a person sees; envelope_from is the address that actually delivered and is where a bounce would go. They differ for mailing lists and forwarders, and code that checks only one will eventually be surprised.There is no IMAP uid in message.received. The event fires the moment the mail server accepts the message, which is before the mailbox has assigned one. Use message_id, which is the sender’s and never changes.

Bounce payloads

permanent: true means stop sending to that address. It is surfaced as its own boolean so you do not have to learn AWS’s vocabulary to act on the one distinction that matters. Continuing to send to addresses that hard-bounce is the fastest way to lose a domain’s sending reputation.

Verifying the signature

Two headers arrive with every delivery: The signed string is ${timestamp}.${rawBody}.
Node
Verify against the raw request body, never a re-serialised object.This is the single most common way a webhook integration is got wrong. Parsing the JSON and re-stringifying it can change insignificant whitespace, how a number is written, or how a non-ASCII character is escaped — and any of those produce a different string and a failed verification for a message nobody tampered with.In Express, that means express.raw({ type: "application/json" }) on this route, not express.json(). In Next.js, await request.text() before any parsing.
The timestamp is inside the signed string rather than merely sent beside it. That is what makes the freshness check meaningful: a captured delivery cannot be given a new timestamp without invalidating the signature.

Responding

Answer 2xx quickly. Anything else — including a 3xx — counts as a failure and is retried. We do not follow redirects, so a redirecting endpoint never receives anything. We wait 10 seconds. If your handler needs longer, acknowledge immediately and do the work afterwards; that is what the retry schedule assumes.

Retries

Six attempts, then we stop: About half a day in total — long enough that an endpoint broken over lunch still receives its events once fixed, short enough that a permanently dead one stops costing anything within a day. Disabling an endpoint stops its queued deliveries; they are not retried at you later.

Deduplicate on id

Deliveries are at-least-once. A network failure after your server processed the request but before we saw the response is indistinguishable, from our side, from one that never arrived — so we retry, and you may see the same id twice. Store the id and ignore one you have already handled. We also deduplicate on our side, so a message the mail server retried does not become two separate events, but that protects against a different failure than this one.

Ordering

Events are not ordered. Deliveries run in parallel so one slow endpoint does not delay everybody else’s events, and a retried event arrives after events raised later. Use createdAt if order matters to you.

Errors

Delivery attempts, their response codes and their failure reasons are recorded against each endpoint. An endpoint that never receives anything is usually one of: