# Webhooks: signed pushes, and exactly what is guaranteed

When a verdict is signed on a shipment where **your** leg authenticated, the operator pushes it to your endpoint. When a witness your clears rested on is suspended, you get your own signed recall notice. Nothing is pushed to a member that is not provably a party.

## Subscribe

```bash
curl -X POST $OPERATOR/webhooks/subscribe -H "Authorization: Bearer <member session>" \
  -d '{"url":"https://mirror-hooks.bank.example/deliveries","events":["verdict.signed","recall.notice"]}'
```

Or use `client.webhooks.subscribe(url)` in the [SDK](/sdk.md), or the [Terraform module](/terraform.md).

- **A member session is required.** The shared pilot token names no institution to deliver to. A desk key needs the `webhooks` scope.
- **What the endpoint must be:** https, a public DNS name, no credentials in the URL. IP literals, `localhost`, `.internal` and single-label names are refused. The Node host re-checks the **resolved** address at connect time, so a name re-pointed at a private address is refused too. Redirects are not followed.
- **What you get back:** a **signed receipt** of exactly what you registered.
- **Idempotent:** subscribing the same endpoint again is the same subscription.

## What arrives

`POST` to your URL, body `{record, proof}`:

- `record.type` = `mirror.webhook-delivery`
- `record.id` is the **delivery id**, to dedupe on. It is stable across retries **and** across a retried or failover-resubmitted verdict, because it is derived from the judgement, not the signature bytes (SPEC §8.2).
- `record.to` is your member kid. Refuse anything addressed to someone else.
- `record.payload` is the verdict's own `{record, proof}`, verifiable on its own.
- Headers: `Mirror-Delivery`, `Mirror-Event`, `Mirror-Attempt`.

Verify with `mirror-webhook.verifyDelivery(body, {jwks, me, now_ms, seen})`, or `client.webhooks.verify()`. It checks:

- the operator's signature;
- the recipient;
- freshness (5 minutes);
- the payload's own signature;
- that the id derives from the subscription and the record;
- dedupe.

Answer 2xx, including for a duplicate.

## The guarantees, exactly

| Property | What holds |
|---|---|
| Delivery | **At least once.** Dedupe on `id`. |
| Ordering | **Not guaranteed.** |
| Retries | Attempts at 0 s, 30 s, 2 m, 10 m, 1 h, 6 h and 24 h, each plus up to 10% deterministic jitter. Then `dead`, and still listable. |
| `410 Gone` | Retires the endpoint: the subscription is **paused**. Re-subscribe to reactivate; `/webhooks/redeliver` requeues a dead or gone delivery. |
| First attempt | Starts as soon as the verdict is signed. Measured on the Node host over loopback in the test suite: **under 2 seconds**. On the Worker it runs in the same invocation, after the response. |
| Durability | The outbox is the durable store (`mirror_webhook_deliveries`). Retries run on the Node host's timer and the Worker's one-minute cron trigger. With an in-memory store, a pending retry lives only as long as the process. |
| What it is | A notification. The ledger is the record of truth; a lost push loses nothing. |

## SIEM

A SIEM collector wants its own token, and an operator holding it would hold a credential into the bank. So the operator stores none. Either:

- the SIEM **pulls** `/audit/export` on a schedule (see [SIEM export](/sdk.md));
- or a relay inside the bank verifies deliveries and forwards them with the SIEM's token.
