cherami.
Guides

Receive webhook notifications

Get notified when mail arrives or a send is accepted, verify the delivery, then fetch the message through the API.

Read as Markdown ↗

A webhook is an https URL that Cherami's delivery provider calls when something happens on your account: received mail became ready to read, or an outgoing message was accepted by the sending provider. The notification carries identifiers, not mail. Your receiver verifies the signature, acknowledges, and fetches the message through the authenticated API when it is ready to act.

A webhook does not wake an agent. It wakes whatever answers at the URL: a server, a serverless function, a queue ingress. If nothing of yours runs continuously, polling remains the right fit, and an agent can keep polling even while a receiver collects notifications for later.

Add a webhook

Choose which events you want and which inboxes. message.received fires when a received message reaches ready, so its content can be read at once. message.sent fires when the sending provider's acceptance is persisted; that is submission, not delivery, inbox placement or reading. Nothing else emits events today: feedback to Cherami, deletion-request correspondence and reindexing stay silent.

Everything in this guide can also be done by a signed-in human from Account: the same webhooks with their live state, the signing secret on request, custom headers and delivery history. The examples below use the HTTP API.

Leave inbox_ids out to cover every inbox on the account, including inboxes created later. Name at most 10 inbox IDs to restrict the webhook to those inboxes; the IDs must be live inboxes you own when you make the change.

curl --silent --show-error --request POST "https://cherami.to/v1/webhooks" \
  --header "Authorization: Bearer $CHERAMI_API_KEY" \
  --header 'Content-Type: application/json' \
  --data-binary @request.json
{
  "url": "https://example.com/hooks/cherami",
  "event_types": ["message.received"],
  "inbox_ids": ["INBOX_ID"]
}

The 201 response includes secret, the signing secret for this webhook, shown here and on explicit retrieval only. Store it privately beside the receiver that verifies with it. Adding the same URL again makes a second webhook that receives the same events; if a response is lost, follow add-webhook recovery.

Cherami validates the URL's shape and never fetches it. The delivery provider refuses private and unroutable destinations and does not follow redirects, so point the webhook at the final public address.

What a delivery contains

Every delivery is a POST with a JSON body of this shape:

{
  "id": "66666666-6666-4666-8666-666666666666",
  "type": "message.received",
  "timestamp": "2026-10-01T14:30:00.123Z",
  "data": {
    "inbox_id": "11111111-1111-4111-8111-111111111111",
    "message_id": "22222222-2222-4222-8222-222222222222"
  }
}

id is the Cherami event ID, stable for this event across every delivery attempt and every webhook. timestamp is when the mail transition was persisted. data.message_id is a received message ID for message.received and a sent message ID for message.sent; read it with GET /v1/messages/{message_id} or GET /v1/sent/{message_id}.

The payload carries identifiers only: the delivery provider never processes mail content, and the API returns the whole message.

Verify and acknowledge

Deliveries follow the Standard Webhooks specification. Three headers arrive with each one: webhook-id, webhook-timestamp and webhook-signature (also sent as svix-id, svix-timestamp and svix-signature). Verify before trusting anything in the body:

  1. Build the signed string webhook-id.webhook-timestamp.body, using the raw request body bytes exactly as received. Re-serializing parsed JSON changes whitespace and breaks the signature.
  2. Compute HMAC-SHA256 over that string with the secret, base64-decoded after removing its whsec_ prefix, and base64-encode the result.
  3. Accept if any space-separated v1,<signature> entry in webhook-signature matches with a constant-time comparison. Several entries appear while a rotated secret is still in its overlap window.
  4. Reject timestamps far from the current time to limit replay.

The Standard Webhooks and Svix verification libraries implement exactly this; svix-signature and webhook-signature carry the same values, so either library's verifier works unmodified.

Respond with a 2xx within 15 seconds, once you have durably accepted the event: written it to your queue or store, not finished acting on it. A slow receiver that processes inline times out and is retried.

Deduplicate on the payload id. Delivery is at-least-once and unordered: the same event can arrive more than once, and the webhook-id header can differ between those deliveries, so key deduplication on the body's id and use timestamp when sequence matters.

Handle failures

The provider retries failed deliveries on a schedule spread over about a day, then disables a webhook that keeps failing. A disabled webhook reads status: "disabled" with enabled: false and stays that way until you edit it with enabled: true; nothing re-enables automatically. Repair the receiver first, then re-enable, then catch up on anything missed by polling the inbox for messages after the last event you handled. There is no resend. Mail itself is unaffected while the receiver is down: it is stored and readable through the API as usual.

See what was delivered

Three reads show the provider's view of delivery, for confirming an integration or explaining a missing notification:

  • Attempts for a webhook: each attempt toward that URL, newest first, with status, HTTP response code, duration, the event it carried and the start of the receiver's response as plain text.
  • Recent events: what the account handed to delivery, whichever webhooks it reached. Confirms that mail activity produced an event before you debug the receiver.
  • One event by ID: the event and every attempt made for it across your webhooks. Start here when a receiver reports a duplicate or a gap.

History covers the provider's retained window only; an older event reads as unavailable. A success attempt is a timely 2xx, nothing more. The response snippet is the start of whatever your receiver answered, shown as text.

Edit, rotate, delete

Event types, inbox scoping and the enabled flag are editable with absolute values: send the whole event_types or inbox_ids array you want, and an empty inbox_ids array returns the webhook to every inbox. Scoped inboxes you later delete remain listed under deleted_inbox_ids until you re-scope; they match nothing. The URL cannot be edited. Add the replacement webhook, confirm it receives, then delete the old one.

Custom headers are sent verbatim with every delivery. Use one to carry a shared token the receiver checks in addition to the signature, or routing metadata. Header names can be listed later; values are never returned.

Rotating the secret returns a new one. By default the replaced secret keeps signing for 24 hours alongside the new one, so update the receiver at your pace; immediate: true expires the replaced secret at once.

Deleting a webhook stops future deliveries and does not touch mail.

Agents and MCP hosts

Webhooks are managed through the HTTP API or from Account; the MCP server does not expose them yet. An agent connected over MCP still benefits from one on the account it is connected to, but a webhook will not call back into the agent's conversation. Pair a webhook with something that can act on it: a receiver that queues work for the next agent run, or an automation runtime that starts the agent. The automation cookbooks show the polling shape that works without any receiver.

Webhook operations reference · Errors, limits and pagination

On this page