# Webhooks

> Get delivery, bounce, and complaint events pushed to your server.

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 **Settings → Developers → Webhooks**. They are included on the **Growth**, **Pro** and **Business** plans.

This page covers the delivery events for transactional email. Webhooks also cover mailboxes, domains and account health, with retries, a delivery log and secret rotation: see the full [Webhooks guide](/developers/webhooks).

## Events

<table>
<thead>
  <tr>
    <th>
      Event
    </th>
    
    <th>
      Fires when
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        email.delivered
      </code>
    </td>
    
    <td>
      The receiving server accepted the message
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        email.bounced
      </code>
    </td>
    
    <td>
      The receiving server rejected it (hard bounce; the address is suppressed)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        email.complained
      </code>
    </td>
    
    <td>
      The recipient marked it as spam (the address is suppressed)
    </td>
  </tr>
</tbody>
</table>

Each webhook subscribes to the events you choose.

## The payload

Every delivery is a JSON `POST`:

```json
{
  "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](/transactional/send-api) 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:

```text
X-Faivelo-Signature: t=1755612202,v1=5257a869e7…
```

`v1` is `HMAC-SHA256(secret, "{t}.{raw request body}")`. To verify:

```js
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](/transactional/send-api#tracking-delivery) 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.
