# Webhooks and events

Every state change in a matter emits an event. Events reach you two ways: pushed to registered webhook endpoints, signed and retried until they arrive, and replayable on demand per escrow. You never have to poll, and you never have to trust that you did not miss one.

## The event catalog

| Event | Fires when |
|---|---|
| `escrow.created` | Terms are agreed and the escrow exists |
| `escrow.funded` | Funds are locked for the matter |
| `delivery.submitted` | A signed delivery arrives |
| `verdict.passed` | Deterministic verification accepted the delivery |
| `verdict.failed` | Deterministic verification rejected the delivery |
| `dispute.opened` | A party challenges the outcome |
| `dispute.evidence` | A party submits content-addressed evidence |
| `dispute.resolved` | A ruling is recorded; a split ruling carries its shares |
| `escrow.refunded` | A refund is initiated |
| `escrow.timeout` | A stage deadline passed in silence; the sweep resolves it |
| `custody.failed` | Custody could not settle; the matter awaits retry, never silently |
| `quitanza.issued` | The terminal proof exists. The flagship event |

Every event has the same shape: `{ event, escrowId, at, data }`, where `data` carries the event's specifics, e.g. the quitanza id on `quitanza.issued`.

## Registering an endpoint

```ts
const { signingKey } = await fetch(`${base}/v1/webhooks`, {
  method: "POST",
  headers: { "content-type": "application/json", authorization: `Bearer ${key}` },
  body: JSON.stringify({ url: "https://example.test/hooks/quitanza" })
}).then((r) => r.json());
// Pin signingKey: every delivery is verified against it.
```

Each registered URL receives every event as a POST with the canonical JSON body and two headers: `x-quitanza-signature`, the Ed25519 signature over that body, and `x-quitanza-key-id`, the signing public key.

## Verifying a delivery

Use the SDK's `verifyWebhook` with the raw request body, before any JSON parsing of your own:

```ts
import { verifyWebhook } from "@quitanza/sdk";

const event = verifyWebhook(rawBody, req.headers, { signingKey });
if (event === null) return res.status(401).end(); // forged or corrupted
if (event.event === "quitanza.issued") {
  // the matter closed in proof
}
```

Pin `signingKey` to the key announced at registration, so a forged key id cannot pass.

## Delivery guarantees

A failed delivery retries with backoff: up to 5 attempts at roughly 1s, 5s, 25s and 60s intervals. An endpoint that never accepts the event lands it on the dead-letter list, inspectable at `GET /v1/webhooks/dead-letters`. Nothing is dropped quietly.

## Replay

Webhooks are a convenience, not the record. The record is the escrow's hash-chained evidence trail, and every event is replayable from it:

```ts
const { events } = await qz.escrows.events(escrow.id);
```

`GET /v1/escrows/{id}/events` returns the full ordered history exactly as webhooks received it. Because it is derived from the trail, a replay can never disagree with the tamper-evident record. If your endpoint was down for an hour, replay the escrows you care about and reconcile.
