Webhooks
Event delivery signed with the same key set as verdicts — verify a webhook the way you verify an approval.
Webhooks tell your systems what happened without polling. Delivery is signed the same way verdicts are: every event arrives as an ES256 JWT, verifiable against the public key set at verdicts.saifuro.com/.well-known/jwks.json. One verification routine covers both, and you never have to take an inbound request's word for where it came from — there is no shared webhook secret to store or leak.
Register an endpoint
| Field | Type | Required | Meaning |
|---|---|---|---|
url | string | yes | HTTPS endpoint on your side |
events | array | yes | Which event types to deliver, from the table below |
{
"webhook": "whk_4b09",
"url": "https://ops.example.com/saifuro",
"events": ["decision.review_pending", "escrow.disputed"],
"status": "active",
"created_at": "2026-09-01T10:00:00Z"
}GET /v1/webhooks, GET /v1/webhooks/{id}, and DELETE /v1/webhooks/{id} complete the surface. POST /v1/webhooks/{id}/ping sends a ping event to test the path end to end.
Delivery
An event is an HTTP POST to your URL with Content-Type: application/jwt and the token as the body. Decoded, the claims are:
{
"iss": "https://verdicts.saifuro.com",
"jti": "evt_a91c",
"iat": 1788258300,
"type": "escrow.disputed",
"data": {
"escrow": "esc_2f8d",
"state": "funded",
"disputed": true
}
}Verify it in the same three steps as a verdict — signature against the JWKS, issuer, and then look at the claims. jti is the event id and stays the same across retries, so a duplicate is detectable by the id you have already processed.
Respond 2xx to acknowledge. Anything else, and delivery retries with backoff for 24 hours, after which the event is dropped from delivery but stays readable in the log.
data carries the object the event is about, in the same shape the API returns it — enough to act on, and the id to fetch the rest.
Event types
| Type | Fires when |
|---|---|
decision.review_pending | A verdict came back review and waits for the mandate owner |
decision.resolved | A review was approved, denied, or expired |
mandate.revoked | A mandate was revoked; the next request under it returns deny_revoked |
mandate.expiring | Seven days before a mandate's expires date |
escrow.funded | Buyer's funds locked in the contract |
escrow.delivered | The condition was attested |
escrow.released | Funds moved to the seller |
escrow.refunded | Funds returned to the buyer |
escrow.disputed | A party disputed the deal — see escrow |
ping | You asked for one |
Events describe state that already changed. Missing a webhook loses you a notification, never a record: everything here is reconstructable from decisions and exports.

