How it works
Register the subscription
Register a webhook via the API or the Sendmux app, telling Sendmux which event types you care about and which HTTPS URL to POST them to.
Capture the signing secret
We return a signing secret in that one response. Store it securely — we never return it again.
Receive event POSTs
When a matching event fires, Sendmux POSTs a JSON body to your URL with four headers identifying the event and its signature.
Verify, process, ack
Your endpoint verifies the signature, processes the event, and returns
2xx within 10 seconds.Create a subscription
CallPOST /webhooks with the target URL and the event types you care about.
secret — the HMAC key used to sign every event posted to your endpoint. Copy it somewhere safe.
Verify the signature
Always verify the signature before acting on an event. Without verification, a motivated attacker could forge events and trick your systems into processing bounces or inbound messages that never happened. See Verify webhook signatures for ready-to-paste Node, Python, and Go examples.Retries
If your endpoint returns a non-2xx status or doesn’t respond within 10 seconds, we retry with an exponential back-off schedule: 30 s → 2 min → 10 min → 30 min → 1 hr → 4 hr → 12 hr → 24 hr. That’s eight attempts across a 24-hour window.
After the final attempt fails, we set failing to true on the subscription and email the team owner. Events that fire while a subscription is marked failing are skipped — you’ll see gaps in your event history until you fix the endpoint and toggle enabled off then back on via PATCH /webhooks/{id} — the false → true transition clears the failing flag so retries resume on the next matching event. Changing the URL alone does not clear the flag.
Rotate the signing secret
If a secret leaks or you simply want to rotate periodically, callPOST /webhooks/{id}/rotate-secret. The response includes the new secret field (returned exactly once), and the next event delivery is signed with the new value. The old secret is invalidated immediately.
Send a test event
POST /webhooks/{id}/test publishes a synthetic sendmux.test event scoped to your team. The response contains the event_id so you can correlate receipt on your endpoint via X-Sendmux-Event-Id.
Reference
The remainder of this page is a contract reference for the wire format.Event types
Sendmux defines the event types below. Subscribe to just the ones you need.| Event type | When it fires |
|---|---|
ses.bounce | A message you sent was bounced by the recipient mail server. |
ses.complaint | A recipient marked one of your messages as spam. |
ses.delivery | A message was successfully delivered to the recipient mail server. |
ses.reject | Our outbound platform refused to send the message (for example, rate-limited or reputation-blocked). |
ses.delivery_delay | A recipient mail server is temporarily unavailable — we’re still retrying. |
mail.inbound | An inbound message arrived at one of your mailboxes. Fires for both normal mail and spam-classified mail. |
sendmux.test | A synthetic event emitted by POST /webhooks/{public_id}/test so you can verify end-to-end delivery. |
Delivery headers
Every webhook POST carries these HTTP headers.| Header | Value |
|---|---|
Content-Type | application/json |
X-Sendmux-Signature | sha256=<hex-hmac-sha256(body, secret)> |
X-Sendmux-Event-Id | evt_<cuid2> — unique per event, stable across retries |
X-Sendmux-Event-Type | One of the event types in the table above |
X-Sendmux-Delivery-Attempt | 1 on the first delivery; increments on each retry |
Delivery body
The request body is the canonical event envelope.| Field | Description |
|---|---|
id | Stable per-event identifier. Also sent in the X-Sendmux-Event-Id header. |
source | "ses" for outbound delivery events, "mail" for inbound, "sendmux" for synthetic test events. |
event_type | The dotted {source}.{type} form — e.g. "ses.bounce", "mail.inbound". Mirrors the value of the X-Sendmux-Event-Type header. |
type | Event subtype without the source prefix — e.g. "bounce", "inbound", "test". |
raw_source | The event payload as Sendmux first observed it. Mirrors the upstream provider’s payload verbatim, so its nested keys reflect the upstream contract (often camelCase) — use this when you need a field we haven’t surfaced in normalised. |
normalised.is_spam | Populated only for mail.inbound — true for spam, false for ham, null for every other event type. |
Permissions
Webhook endpoints are gated by the following API-key permissions. Keys with the wildcardwebhook.* satisfy all of them, and the built-in root:full role includes webhook.*. A dedicated root:webhook_admin preset is available for keys that manage only webhooks.
| Endpoint | Required permission |
|---|---|
POST /webhooks | webhook.create |
GET /webhooks | webhook.read |
GET /webhooks/{public_id} | webhook.read |
PATCH /webhooks/{public_id} | webhook.update |
DELETE /webhooks/{public_id} | webhook.delete |
POST /webhooks/{public_id}/rotate-secret | webhook.manage |
POST /webhooks/{public_id}/test | webhook.manage |
Keys provisioned with
root:readonly carry webhook.read so monitoring integrations can list your subscriptions without being able to modify them.