Outbound webhooks (the firehose)
Implemented in #240. (The older
webhooks.mdis the original planning note.)
Izri POSTs a signed JSON payload to any HTTPS endpoint you register whenever an event happens on one of your organization's projects. This is the self-serve "escape hatch" — any customer can build their own integration in an afternoon, and our first-party integrations (Slack, Linear, …) consume the same firehose.
Manage endpoints under Organization → Settings → Webhooks, or via the
outboundWebhooks tRPC router (create / list / update / rotateSecret /
delete / test / listDeliveries).
Events
| Event | Fires when |
|---|---|
delta_report.updated |
A delta report is (re)computed for a change set — the headline "something happened on this PR" signal. |
scope_check.completed |
A scope check finishes. (reserved — accepted as a subscription filter, not yet emitted) |
test_run.completed |
A test run reaches a terminal state. (reserved — accepted as a subscription filter, not yet emitted) |
An empty eventTypes list receives all events; otherwise only the listed
ones. (finding.created is intentionally not offered — findings are embedded in
check rows, not first-class entities.)
Request format
POST <your-url>
Content-Type: application/json
User-Agent: izri-webhooks/1
X-Izri-Event: delta_report.updated
X-Izri-Delivery: <ULID> # unique per delivery; use it to dedupe
X-Izri-Signature-256: sha256=<hex>
The body is event-specific JSON. For delta_report.updated:
{
"changeSetId": "01H…",
"deltaReportId": "01H…",
"umbrellaState": "passing", // passing | failing | unknown
"status": "complete", // pending | partial | complete | error
"alignmentScore": 87, // 0-100 | null
"testPassRate": 1.0, // 0-1 | null
"hallucinationScore": 92, // 0-100 | null
"visualDiffScore": 100 // 0-100 | null
}
Verifying the signature
X-Izri-Signature-256 is sha256= + the hex HMAC-SHA256 of the raw request
body using your subscription's signing secret (shown once at creation; rotate
it any time). This mirrors GitHub's scheme.
import { createHmac, timingSafeEqual } from 'node:crypto'
function verify(rawBody: string, header: string, secret: string): boolean {
const expected = `sha256=${createHmac('sha256', secret).update(rawBody).digest('hex')}`
const a = Buffer.from(expected)
const b = Buffer.from(header)
return a.length === b.length && timingSafeEqual(a, b)
}
Always compute the HMAC over the exact bytes you received, before any JSON parsing.
Delivery, retries, and auto-disable
- At-least-once. A delivery may arrive more than once. Dedupe on
X-Izri-Delivery. - Retries. A non-2xx response or network error is retried with exponential
backoff (2s, 4s, 8s, …) up to 5 attempts, then the delivery is marked
failed. - Auto-disable. After 10 consecutive failed deliveries the subscription is disabled so a dead endpoint stops costing retries. Any success resets the counter; re-enabling a disabled subscription clears the streak.
- Delivery log. The last 50 attempts per subscription are retained
(
webhook_deliveries) and surfaced in the dashboard for debugging.
Respond 2xx quickly (under a few seconds); do heavy work asynchronously.