---
title: Webhooks
description: Receive a POST when Izri emits an event — the three event types, signature verification, and the retry model.
order: 50
---

# Webhooks

Outbound webhooks POST to your endpoint whenever Izri emits an event for one of your organization's projects. Every attempt is logged.

## Event types

Three, currently:

| Event | Fires when |
| --- | --- |
| `delta_report.updated` | The aggregate verdict for a delta changed. |
| `scope_check.completed` | A scope analysis finished. |
| `test_run.completed` | A test run reached a terminal status. |

`delta_report.updated` is the one most integrations want — it is the umbrella verdict changing, which is the thing worth reacting to.

A subscription with an **empty** event list receives **all** events. Listing events explicitly narrows it.

There is no `finding.created`. Findings are JSONB arrays on check rows rather than first-class entities, so there is no clean emit point for them yet.

## Verifying the signature

Every request carries an HMAC over the exact bytes posted, in the `x-izri-signature-256` header, formatted `sha256=<hex>`. Same scheme as GitHub's.

```ts
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)
}
```

Two things to get right:

- **Sign the raw body**, not a re-serialized object. `JSON.parse` followed by `JSON.stringify` can reorder keys or change whitespace, and the HMAC won't match. Capture the raw bytes before any body parser runs.
- **Compare in constant time.** `===` on a signature leaks timing.

## Retries

An attempt that doesn't succeed is retried with exponential backoff, starting at 2 seconds and doubling: **2s, 4s, 8s, 16s, 32s**, up to **5 attempts**. After that the delivery is marked `failed`.

A subscription that fails **10 consecutive times** is disabled to stop Izri hammering a dead endpoint indefinitely. Re-enable it from the dashboard once your endpoint is healthy.

## Make your handler idempotent

Retries mean your endpoint will occasionally receive the same event twice — a delivery that timed out after your handler committed still counts as a failure and gets retried. Key on the delta or run identifier in the payload and make reprocessing a no-op.

## Respond fast

Acknowledge with a 2xx as soon as you've persisted the payload, then do the real work asynchronously. A slow handler burns through the retry budget on work that already succeeded.

## Related

- [Signals overview](/docs/signals/overview) — what a verdict change actually means.
- [REST API](/docs/reference/rest-api) — the rest of the HTTP surface.
