Webhooks
Get a signed HTTP request every time one of your emails is sent, delivered, bounced, opened or clicked.
A webhook is a URL on your server that Flaresend calls when something happens to an email. Instead of polling Retrieve an email, you get a POST with a JSON body seconds after the event.
Typical uses:
- Mark a user's address as bad when a welcome email bounces.
- Stop emailing someone who marked you as spam (Flaresend already suppresses them; your app may want to know too).
- Record opens and clicks in your own analytics.
- Alert on
email.failed.
How it works
- You register an endpoint URL and pick the event types it should receive.
- Flaresend creates a signing secret that starts with
whsec_. It is shown once, when the webhook is created or its secret is rotated. - Every time an email in the project gets a matching event, Flaresend queues one delivery per matching webhook and
POSTs the event to your URL. - Your endpoint checks the signature, handles the event, and answers with any
2xxstatus within 10 seconds. - Anything else is retried for about a day.
Webhooks belong to a project. Only events for emails in that project are sent.
Create a webhook
Open your project, go to Webhooks, and click Add webhook. Enter the URL, pick the events, and save. Copy the signing secret from the dialog: it is not shown again.
events defaults to ["*"], which means every event type. enabled defaults to true. The URL must start with http:// or https://.
See Create a webhook for the full request.
Handle the event
Read the raw request body, verify the Flaresend-Signature header, then parse the JSON. Answer quickly: if your work is slow, store the event and process it in the background.
import express from 'express';
import { verifyWebhookSignature, type WebhookPayload } from '@flaresend/client';
const app = express();
// express.raw keeps the exact bytes. Do not use express.json() on this route.
app.post('/api/flaresend-webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const ok = await verifyWebhookSignature(
process.env.FLARESEND_WEBHOOK_SECRET!,
req.header('Flaresend-Signature'),
req.body, // a Buffer, which is a Uint8Array
);
if (!ok) return res.status(401).send('bad signature');
const event = JSON.parse(req.body.toString('utf8')) as WebhookPayload;
if (event.type === 'email.bounced') {
await markAddressBad(event.data.recipient);
}
res.sendStatus(200);
});Events can arrive more than once
A delivery is retried when your endpoint doesn't answer with 2xx in time, even if it did the work. Use the event id (also sent as the Flaresend-Event-Id header) to skip events you have already handled.
The payload
Every event has the same outer shape:
{
"id": "evt_01K6B2ZC1W7Q3T9M4X8V2N5R6P",
"type": "email.delivered",
"createdAt": "2026-09-26T14:02:11.482Z",
"data": {
"emailId": "email_01K6B2Y4ZP9R3M7T8V5N2QXW4C",
"recipient": "ada@example.com",
"from": "hello@acme.com",
"subject": "Welcome to Acme",
"tags": { "kind": "welcome" },
"delivery": { "status": "delivered", "provider": "gmail", "deliveryTimeMs": 1234 }
}
}data always has emailId, recipient (or null when the event is about the whole email), from, subject and tags. The other fields depend on the type. Event types lists them all.
Test it
Send a test event from the dashboard (Send test event on the webhook's page) or with POST /v1/webhooks/:id/test. It delivers a synthetic email.delivered event with "test": true in data, signed like a real one. The webhook must be enabled.
Manage webhooks
| Task | API | SDK |
|---|---|---|
| List | GET /v1/webhooks | flaresend.webhooks.list() |
| Change URL, events or on/off | PATCH /v1/webhooks/:id | flaresend.webhooks.update(id, input) |
| New secret | POST /v1/webhooks/:id/rotate-secret | flaresend.webhooks.rotateSecret(id) |
| See recent attempts | GET /v1/webhooks/:id/deliveries | flaresend.webhooks.deliveries(id) |
| Delete | DELETE /v1/webhooks/:id | flaresend.webhooks.remove(id) |
Turning a webhook off (enabled: false) stops new deliveries. Deliveries still waiting for a retry are marked failed with the response body webhook disabled when their turn comes.