Quitanza

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.

This page as markdown