Skip to content

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

EventFires when
email.deliveredThe receiving server accepted the message
email.bouncedThe receiving server rejected it (hard bounce; the address is suppressed)
email.complainedThe 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.