DMARC Examiner Webhooks API
Subscribe to events via outbound HTTPS webhooks — Pro plan and above. ## Events emitted The system currently emits a single event: - **`alert.created`** — fired whenever an Alert is created in the organization (triggered by report processing and DNS change detection). Other event types (e.g. `report.received`, `domain.verified`) are NOT emitted today. If your integration depends on them, treat them as out of scope until officially announced. ## Delivery & signing Each delivery is a `POST` to the configured `url` with the JSON body matching `WebhookEvent` (see the schemas section) and the following headers: | Header | Value | |---|---| | `Content-Type` | `application/json` | | `User-Agent` | `DMARC-Examiner-Webhooks/1.0` | | `X-Webhook-Timestamp` | Unix epoch seconds when the request was signed | | `X-Webhook-Signature` | `sha256=` — see signing below | ### How the signature is computed ``` signed_value = "{X-Webhook-Timestamp}.{raw_request_body}" signature = hex(HMAC-SHA256(webhook.secret, signed_value)) header = "sha256=" + signature ``` The signature covers BOTH the timestamp and the exact body bytes. The shared `secret` (returned only at create time and re-fetched via `GET /webhooks`) **never travels on the wire** — only the derived signature does. ### How to verify on your side 1. Reject requests where `|now - X-Webhook-Timestamp| > 5 minutes` (rejects replays). 2. Recompute the expected signature with your stored `secret` and the raw body you received. 3. Compare with the value after `sha256=` using constant-time string comparison (`crypto.timingSafeEqual` in Node, `hmac.compare_digest` in Python, `hash_equals` in PHP). **Do NOT use `==`.** 4. Only after the signature matches, trust the body and parse it as `WebhookEvent`. ## Retries Failed deliveries (network error or non-2xx response) are retried twice with delays of 30 s and 120 s. After 10 consecutive failures the webhook is auto-disabled (`is_active` set to `false`) and no further deliveries are attempted until you re-enable it via `PUT /webhooks/{id}`. Successful deliveries record the timestamp in `last_triggered_at`, the HTTP status in `last_status`, and reset `failure_count` to `0`. The `POST /webhooks/{id}/test` endpoint does NOT touch any of those fields.