# Webhooks

Receive account events at your URL and verify their signatures.

## How webhooks work

Webhooks push account events to your server the moment they happen — no polling. Email Bump sends a signed `POST` with a JSON body and retries with backoff until your endpoint returns a `2xx`. Every attempt is logged so you can debug delivery.

Webhook URLs must resolve to a public address. Anything pointing at `localhost`, a private range, or a cloud metadata address is refused when you save it and again when a delivery is attempted — a name that resolves somewhere private later doesn't get a second chance. Redirects aren't followed, so the URL you give us is the only place we send.

## Subscribe to events

1. **Add an endpoint** — Go to **Developers → Webhooks** and add your HTTPS URL.
2. **Pick events** — Subscribe to specific events, or leave them all unchecked to receive everything.
3. **Save the signing secret** — It's shown once on creation — store it to verify incoming requests.
4. **Send a test** — Use **Send test event** and watch it appear under the endpoint's deliveries.

## Payload shape

Every delivery is a JSON envelope with the event type and its data:

```json
{
  "id": "dfe336e3-9730-4eb4-8841-7b0bbd766dda",
  "type": "contact.created",
  "created": "2026-07-21T19:56:38Z",
  "data": {
    "contact": {
      "id": "5fef0867-b292-46ca-8371-e24d8575576c",
      "email": "jamie@example.com",
      "subscribed": true
    }
  }
}
```

Two headers accompany every request:

```
Email-Bump-Event: contact.created
Email-Bump-Signature: t=1784663798,v1=acf8fe8e05a9fde3347ad708ac3640c9…
```

## Event types

- `contact.created`, `contact.updated`, `contact.deleted`
- `contact.subscribed`, `contact.unsubscribed`
- `email.delivered`, `email.opened`, `email.clicked`
- `email.bounced`, `email.complained`
- `email.received` — [inbound mail](https://emailbump.com/docs/inbound-api) arriving
- `campaign.sent`

That's the complete list — subscribing to none means you receive all of them.

## Verify signatures

The `Email-Bump-Signature` header is `t=<timestamp>,v1=<hmac>`, where the HMAC is a SHA-256 of `{timestamp}.{raw_body}` keyed with your endpoint secret. Recompute it and compare before trusting a request:

```js
import crypto from "node:crypto";

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("="))
  );
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(parts.v1)
  );
}
```

> ⚠️ **Use the raw request body** — Verify against the exact bytes you received, before any JSON re-serialization — re-encoding can change the body and break the signature.
