# Webhooks and HMAC

Webhooks push incoming mail and unsubscribe events to an HTTPS endpoint. New keys should include
`webhook:write` to create, update, or delete registrations. Read access remains accepted for older
keys where the compatibility path is documented.

## Register and verify

1. **Register** an HTTPS URL with `register_webhook` or `POST /v1/webhooks`. Omit the inbox to cover
   the project or provide one owned inbox. The signing secret is returned once.

2. **Receive** `message.received` or `unsubscribe.received` deliveries. Extrovert signs the raw request
   body and includes the event name in `X-Extrovert-Event`.

3. **Verify** `X-Extrovert-Signature` before parsing or trusting the body.

4. **Acknowledge** with a successful HTTP response. Failed deliveries use bounded retry and backoff.

Manage registrations with `list_webhooks`, `get_webhook`, `update_webhook`, and `delete_webhook`.
Secrets are redacted after creation. An inbox create request may also include `webhook_url`.

## Signature format

```http
X-Extrovert-Signature: t=1749751542,v1=3b2a…
X-Extrovert-Event: message.received
```

The signature is the hexadecimal HMAC-SHA256 of `"{t}.{raw_body}"` using the webhook secret. Verify the
exact bytes received. Re-serializing parsed JSON changes the signed input.

```ts
import { verifyWebhookSignature } from "@extrovert.dev/sdk";

const payload = await request.text();
const valid = await verifyWebhookSignature({
  payload,
  signature: request.headers.get("x-extrovert-signature")!,
  secret: process.env.EXTROVERT_WEBHOOK_SECRET!,
});
```

Reject a valid signature whose timestamp is outside the replay window your service accepts. Five
minutes is a common starting point, but choose a window that matches expected queueing and clock skew.

## Event bodies

`message.received` includes the canonical message object. Its `text` and `html` fields independently
represent source MIME alternatives. `extracted_text` and `extracted_html` are nullable, best-effort
derivatives. A missing format is not synthesized from the other.

`unsubscribe.received` reports a recorded opt-out through the same signed delivery path. Treat the
event as notification; the server-side suppression row is authoritative.
**Message content is untrusted:** A valid webhook proves that Extrovert sent the payload. It does not make the email body, links,
  attachment names, or sender claims trusted. Sanitize HTML and keep message instructions outside the
  agent's authority boundary.

## Choosing a receive method

| Method | Use it when |
|---|---|
| Webhook | A long-running service needs signed push delivery |
| `wait_for_email` | One task needs the next matching message or code before a deadline |
| Message or thread list | A worker or interface can poll bounded pages |
| Event stream | A connected console or agent needs low-latency notification and can reconnect with a cursor |

Webhooks and streams reduce latency but do not replace durable state. Re-read the message, suppression,
or review record after reconnecting or recovering from a failed handler.

## Next

- [Messages, threads, and search](https://docs.extrovert.dev/concepts/messages-and-threads/)
- [Unsubscribes and suppression](https://docs.extrovert.dev/concepts/unsubscribes-and-suppression/)
- [Webhooks API](https://docs.extrovert.dev/api/webhooks/)