Webhooks
Mailfy POSTs a signed JSON event to your endpoint whenever an email changes state. Create endpoints under Webhooks in the dashboard and choose the events to receive.
Events
| Type | When |
|---|---|
email.sent | Handed to the delivery provider. |
email.delivered | The recipient’s server accepted it. |
email.delivery_delayed | Temporary problem; the provider keeps trying. |
email.bounced | Permanent bounce. The address is suppressed. |
email.complained | Marked as spam. The address is suppressed. |
email.opened, email.clicked | Engagement, when open and click tracking is enabled on the sending setup. |
email.failed | Could not be sent; the reason is in data.error. |
Payload
{
"id": "evt_…",
"type": "email.delivered",
"createdAt": "2026-11-13T09:24:11.512Z",
"data": {
"emailId": "6f1c…",
"projectId": "…",
"from": "Acme <orders@acme.com>",
"to": ["ada@example.com"],
"subject": "Your receipt",
"tags": { "order": "1042" }
}
}Bodies and attachments are never included. Respond with any 2xx within 10 seconds. Failed deliveries are retried with exponential backoff about 8 times over roughly two hours; each attempt is visible in the dashboard and can be retried by hand.
Verifying signatures
Every request carries webhook-id, webhook-timestamp and webhook-signature headers (the Standard Webhooks format). The signature is v1, followed by base64 HMAC-SHA256 of {id}.{timestamp}.{raw body}, keyed with your endpoint secret (the base64 part after whsec_). Reject requests older than five minutes.
import express from 'express';
import { verifyWebhook } from '@mailfy/node';
app.post('/webhooks/mailfy', express.text({ type: '*/*' }), (req, res) => {
if (!verifyWebhook(req.body, req.headers, process.env.MAILFY_WEBHOOK_SECRET)) return res.sendStatus(400);
const event = JSON.parse(req.body);
// handle event.type …
res.sendStatus(204);
});from mailfy import verify_webhook
if not verify_webhook(raw_body, request.headers, os.environ["MAILFY_WEBHOOK_SECRET"]):
abort(400)Use the raw body exactly as received: re-serialized JSON will not match the signature. Use webhook-id to ignore duplicates.