# Delivery — email & webhooks

> Alerts arrive by email, and by webhook when you configure one: a signed JSON payload you verify, retried with backoff until your endpoint acknowledges it.

- **HTML:** https://offendersearch.app/docs/monitoring/delivery
- **Base URL:** https://api.offendersearch.app
- **Authentication:** `X-API-Key` request header
- **OpenAPI:** https://offendersearch.app/openapi.json · https://offendersearch.app/openapi.yaml
- **Monitoring reference as markdown:** https://offendersearch.app/docs/monitoring.md

## Two channels

Alerts are delivered to the `channels` you set on the monitor. Email is the default; a webhook is added when you set `webhookUrl`. You can set either or both, and at least one is required at creation.

| Channel | Set it with | What arrives |
| --- | --- | --- |
| Email | `channels.email` | A human-readable alert email to the address you name, one per event. |
| Webhook | `channels.webhookUrl` | The alert object as a signed JSON POST to your endpoint, one per event. |

## Email

Set `channels.email` to an address and each alert is delivered there as an email. Email is the right channel for a person who reads the alerts; a webhook is the right channel for a system that acts on them.

## Signed webhooks

Set `channels.webhookUrl` and each alert is POSTed to that URL as JSON — the same alert object the history endpoint returns. Respond `2xx` promptly to acknowledge. Every delivery is **signed** so you can verify it came from us: verify the `X-Offendersearch-Signature` header (an HMAC-SHA256 of the raw request body using your signing secret) before trusting a payload.

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

app.post("/hooks/os", express.raw({ type: "*/*" }), (req, res) => {
  const sent = req.get("X-Offendersearch-Signature") ?? "";
  const expected = crypto
    .createHmac("sha256", process.env.OS_WEBHOOK_SECRET)
    .update(req.body)              // the RAW body, before JSON parsing
    .digest("hex");

  if (!crypto.timingSafeEqual(Buffer.from(sent), Buffer.from(expected))) {
    return res.sendStatus(400);
  }

  const alert = JSON.parse(req.body.toString("utf8"));
  enqueue(alert.id, alert);        // at-least-once: de-duplicate on alert.id
  res.sendStatus(200);
});
```

## Retries

A webhook that does not answer `2xx` is **retried with exponential backoff** for several attempts, so a brief outage on your side does not lose an alert. Treat delivery as **at-least-once**: an alert may arrive more than once, so de-duplicate on `alert.id`. An alert that cannot be delivered after every retry is still readable from `GET /v1/monitors/{id}/alerts`, so the history endpoint is the backstop for anything a webhook missed.

> Make your handler idempotent and fast: acknowledge with a `2xx`, then do the work asynchronously. A handler that blocks risks a timeout that looks like a failure and triggers a retry.

---

## Related

- Previous: [Alerts, events & cadence](https://offendersearch.app/docs/monitoring/alerts.md)
- Next: [Billing — one predicate, one charge](https://offendersearch.app/docs/monitoring/billing.md)
- Index: [Monitoring API reference](https://offendersearch.app/docs/monitoring.md)
