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
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:
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:
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.