Docs /features/outbound-webhooks

Outbound webhooks (the firehose)

Implemented in #240. (The older webhooks.md is 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.

Reading this with an agent? /docs/features/outbound-webhooks.md serves the raw markdown.

All docs