Webhooks
Get an HTTP callback — or a Slack message — the moment an email is captured.
Creating a webhook
Webhooks live on the organization, under Settings → Webhooks. Each one has a type (http or slack), a URL, an optional signing secret, and an optional project: leave the project empty and the webhook fires for the whole organization, set it and the webhook only fires for that project.
Webhooks can be disabled without being deleted, which is usually what you want while you are debugging the receiving end.
Events
There is one event today, sent once the captured email has been parsed and indexed:
| Event | Fires when |
|---|---|
email.received | A new email has been captured and is ready to read |
Payload
HTTP webhooks receive a POST with a JSON body. The payload carries a summary, not the full message — fetch the body from the API if you need it.
{ "event": "email.received", "inbox": { "id": "k3n8x2p9qa7c1m4z", "name": "Acme — staging" }, "email": { "id": "e9v2h6r4ld8w3t5n", "subject": "Reset your password", "from_name": "Acme", "from_email": "noreply@acme.dev", "excerpt": "Click the link below to choose a new password…", "received_at": "2026-08-03T09:14:22+00:00" } }
Verifying the signature
If the webhook has a secret, Mailclutch signs the raw request body with HMAC-SHA256 and sends the hex digest in a header:
X-MailClutch-Signature: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b…Compare it against the body exactly as received, before any JSON parsing or re-serialisation, and use a constant-time comparison:
import crypto from 'crypto'; function verify(rawBody: string, signature: string, secret: string) { const expected = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(signature ?? ''); return a.length === b.length && crypto.timingSafeEqual(a, b); }
No secret means no signature header. Set one for anything reachable from the internet.
Retries
A delivery counts as successful on any 2xx. Otherwise Mailclutch retries up to three times, waiting 1 minute, then 5, then 15. Each attempt times out after 15 seconds and is recorded with its status code, duration and error, so you can see the history in the app.
Make your endpoint idempotent — a slow 200 can still be retried if the connection dies — and answer quickly, queueing the real work.
Where we refuse to deliver
Webhook URLs are checked against private and reserved address ranges before every attempt, not only when you save them. A URL that resolves to a private network is refused and the attempt is recorded as failed. This protects other tenants — and you — from Mailclutch being used to reach inside a network it shouldn't.
So a webhook pointing at localhost will not work. During development, use a tunnelling service that gives you a public URL.
Slack
Choose the slack type and paste an incoming-webhook URL from your Slack workspace. Mailclutch posts a formatted message with the project name, the subject, the sender and an excerpt — no configuration on the Slack side beyond creating the incoming webhook.