---
title: "Webhooks"
description: "Tell your endpoint when mail arrives, instead of making you poll."
url: "https://openemail.uk/docs/knowledge/agents/webhooks"
area: "Knowledge base"
category: "Agents, API & MCP"
status: "live"
---

# Webhooks

Tell your endpoint when mail arrives, instead of making you poll.

## Details

- Usable today from Settings → Webhooks and over the API: register an https endpoint, pick which of the twenty events it wants, and copy the whsec_ signing secret, which is shown on creation and on rotation and never again. Deliveries are real signed POSTs raised by the mailbox itself rather than by any API call, so they fire on inbound mail and on opens and clicks whatever sent the message. Sending fires from every surface, and it used to fire from only some: a send through the API, MCP, a template or a rule raised email.sent while a message sent from the app’s own composer did not, because the composer writes to the mailbox directly rather than through the send service that emitted the event. The event is now raised at the mailbox itself, which is where all of them meet, so composing in the app, scheduling for Tuesday and posting to the API are three ways of causing the same webhook. A deferred send says so twice: email.scheduled or email.queued when it is accepted, email.sent when it actually goes, and email.cancelled if you take it back in between. Ten endpoints per mailbox, enforced wherever one is registered rather than only on this screen.
- The events come in three families. Fifteen are about one message: email.received, email.replied, email.sent, email.delivered, email.failed, email.cancelled, email.scheduled, email.queued (the undo-send sibling of scheduled), email.delivery_delayed, email.bounced, email.complained, email.suppressed, email.opened, email.clicked and email.downloaded. email.sent means the sending service accepted the message, email.delivered means the receiving server did, and email.delivery_delayed means it has not arrived yet and is still being retried. email.replied fires alongside email.received when the arriving message answers one already in the mailbox, so a consumer that wants both gets both. email.downloaded fires when a person fetches a file that went out as a download link, with the same classifier keeping scanners and link previewers out of the count, and it names no recipient, because the link is the same for everyone the message went to. Three are about a domain: domain.verified when it starts receiving, domain.sending_changed when its sending verdict moves, and domain.deleted when it is removed, whether you asked or the seven-day reaper dropped it unverified. Two are about the suppression list itself, which is a different thing from email.suppressed: suppression.added when an address goes on, suppression.removed when one is allowed again. Subscribing to none of them means every message event except email.replied, fourteen today, never a family added later, and the API reads that back as ["*"]. Name the events you want if you would rather be explicit. Each delivery carries X-OpenEmail-Signature as t=<unix>,v1=<hex>, an HMAC-SHA-256 over the timestamp, a dot, and the raw body, plus X-OpenEmail-Event and X-OpenEmail-Delivery. Verify against the bytes as they arrived: parsing and re-serialising reorders keys and breaks the signature. The 300-second replay window is the receiver’s to enforce, and the SDK’s verifier defaults to it.
- Registration is refused for anything that is not https or not publicly routable (loopback, RFC1918, link-local, CGNAT and the IPv6 equivalents), and redirects are not followed, so a 3xx is recorded as a failed delivery rather than chased somewhere else. The receiver gets 5 seconds, endpoints are delivered in parallel so ten of them still cost 5 seconds rather than 50, and recent attempts are listed on the page for that endpoint with the response code and how long it took.
- A delivery is attempted up to five times. The first goes out as the event happens; a failure that could plausibly clear on its own is retried after 1 minute, then 5, then 25, then 2 hours, which spreads one event over about two and a half hours. Retries are held as durable work rather than in memory, so a deploy in the middle of that window does not lose them. Only failures worth repeating are repeated: a timeout, a refused connection, 408, 425, 429 or any 5xx. Any other 4xx is the endpoint rejecting the payload deliberately, and asking four more times would be four times the load for the same answer. The event id is minted once and every attempt carries it in X-OpenEmail-Delivery, so a receiver that sees the same id twice can drop the second rather than act on it twice. After 100 events in a row fail every attempt, the endpoint is disabled, the workspace is emailed, and the reason is readable on the endpoint itself. An endpoint answering 410 Gone is disabled on the spot.
- An endpoint that fails 100 times in a row is switched off rather than dialled forever, and everybody with webhooks access is emailed to say so: which one, what the last attempt reported, and that nothing was queued while it was failing. The count is CONSECUTIVE and any delivered attempt resets it, so a bad afternoon last March cannot add up to a disabled endpoint today. Switching it back on clears the count with it. The console distinguishes the two states rather than showing one toggle: an endpoint you turned off looks different from one we turned off.
- Managing endpoints is one job with two front doors. Over the API it is POST /webhooks, the patch, the delete, rotate-secret, test and the delivery log, with a method for each in the SDK; in the app it is Settings → Webhooks, against the same registry rather than a second one. Reading is gated on webhooks:read, so anybody building an integration can see the endpoints and their delivery history (which one fired, what the receiver answered, how long it took) without being the owner. Registering, editing, testing, rotating and deleting need webhooks:write AND ownership of the mailbox, on both surfaces, and that second half is deliberate: an endpoint has no address axis, so it receives every address the workspace holds with subjects and recipients on it, and no permission means “may be sent all of that”. A role that builds integrations and does not read the mail drives it with a workspace key instead.
