Webhooks
Webhooks are the push counterpart of polling a send’s status: register an HTTPS endpoint and Faivelo POSTs delivery events to it as they happen. Use them to mark a message delivered in your app, alert on bounces, or clean bad addresses out of your database the moment they bounce.
Manage webhooks in the dashboard under Transactional → API. Webhooks are part of transactional email, so they’re included on the Pro and Business plans.
Events
| Event | Fires when |
|---|---|
email.delivered | The receiving server accepted the message |
email.bounced | The receiving server rejected it (hard bounce; the address is suppressed) |
email.complained | The recipient marked it as spam (the address is suppressed) |
Each webhook subscribes to the events you choose.
The payload
Every delivery is a JSON POST:
{ "type": "email.bounced", "created_at": "2026-08-19T14:03:22.000Z", "data": { "email_id": "cme1x…", "from": "receipts@yourdomain.com", "to": ["customer@example.com"], "subject": "Your order is confirmed", "template_alias": "order-confirmation", "status": "BOUNCED", "sent_at": "2026-08-19T14:03:20.000Z" }}data.email_id is the id the send call returned. Bounce and complaint events may carry extra detail about the reason. The request also carries an X-Faivelo-Event header naming the event type.
Verifying signatures
Anyone who discovers your endpoint URL can POST fake JSON to it — so verify every request. Each webhook has a signing secret (shown when you create it), and every delivery is signed with an X-Faivelo-Signature header:
X-Faivelo-Signature: t=1755612202,v1=5257a869e7…v1 is HMAC-SHA256(secret, "{t}.{raw request body}"). To verify:
import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(rawBody, header, secret) { const { t, v1 } = Object.fromEntries(header.split(',').map(p => p.split('='))) // Reject stale timestamps to block replay attacks if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex') return v1.length === expected.length && timingSafeEqual(Buffer.from(v1), Buffer.from(expected))}Compute the HMAC over the raw request body, before any JSON parsing — a re-serialized body won’t match.
Delivery behavior
- Your endpoint has 5 seconds to respond with a 2xx status. Respond first, process after — do your database work asynchronously.
- Redirects are not followed; point the webhook at its final URL.
- The dashboard shows each webhook’s last successful delivery and last error, so a misbehaving endpoint is visible at a glance.
- Each event is delivered once — there is no automatic retry queue. Treat webhooks as the fast path and the send status API as the source of truth you can re-query anytime.
Testing
Every webhook has a Send test button in the dashboard. It delivers a real signed email.delivered event with "test": true in the payload — so you can prove your endpoint and signature verification end-to-end without sending an email.