# Webhooks

Werkmail POSTs a JSON envelope to your HTTPS URL when message or suppression events occur. URLs must be public HTTPS (SSRF-guarded). Secrets are shown once (`whsec_…`).

## Create a subscription

```bash
curl -sS -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "ops",
    "url": "https://example.com/hooks/werkmail",
    "events": [
      "message.delivered",
      "message.hard_bounced",
      "message.spam_complaint",
      "message.failed",
      "message.content_blocked",
      "suppression.added"
    ]
  }' \
  "$BASE/api/practices/$PRACTICE_ID/webhooks"
```

List, delete, test, and inspect deliveries:

- `GET /api/practices/{id}/webhooks`
- `DELETE /api/practices/{id}/webhooks/{webhookID}`
- `POST /api/practices/{id}/webhooks/{webhookID}/test`
- `GET /api/practices/{id}/webhooks/{webhookID}/deliveries`

## Envelope

```json
{
  "id": "delivery-uuid",
  "event": "message.delivered",
  "timestamp": "2026-08-21T12:00:00Z",
  "data": {
    "message_id": "…",
    "status": "delivered"
  }
}
```

## Headers

| Header | Meaning |
| --- | --- |
| `Content-Type` | `application/json` |
| `X-Werkmail-Event` | Event name |
| `X-Werkmail-Timestamp` | Unix seconds |
| `X-Werkmail-Signature` | Hex HMAC-SHA256 of `{timestamp}.{body}` using the webhook secret |
| `User-Agent` | `werkmail-webhooks/1.0` |

Verify in constant time. Reject timestamps older than a few minutes to stop replays.

```python
import hmac, hashlib

def valid(secret: str, timestamp: str, body: bytes, signature_hex: str) -> bool:
    mac = hmac.new(secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256)
    return hmac.compare_digest(mac.hexdigest(), signature_hex.lower())
```

## Event names

Message: `message.created`, `message.sent`, `message.delivered`, `message.opened`, `message.clicked`, `message.hard_bounced`, `message.soft_bounced`, `message.auto_replied`, `message.spam_complaint`, `message.failed`, `message.content_blocked`, `message.suppressed`, `message.unsubscribed`, `message.delivery_delayed`, `message.subscription`.

Suppression: `suppression.added`, `suppression.removed` (include `scope`, `project_id`, `route_id`, and slugs when known).

Test: `webhook.test`.

Open, click, and delay events require open/click tracking on the route or send settings.

## Slack / Teams alerts

`POST /api/practices/{id}/alerts` with `kind` `slack` or `teams` and a public incoming-webhook URL. Default events: hard bounce, soft bounce, complaint, content blocked, failed send.
