Payload structure
Every webhook delivery follows the same envelope format:
The payload is intentionally minimal. When your handler needs more context (parties, legs, timestamps, transitions), fetch the settlement with
GET /v1/settlements/{settlement_id} or its event history with GET /v1/settlements/{settlement_id}/events.
When webhooks fire
Every settlement state transition emits an event, namedsettlement.state. plus the lowercased state name. There is no filtering on KeyStone’s side beyond the patterns you subscribe to.
This holds because every state transition goes through one function, which enqueues the webhook in the same database transaction as the state change. A transition cannot be committed without its announcement being committed alongside it.
The one state that is not reached by a transition is INSTRUCTED, which is written when the settlement row is created. It emits no webhook. The response to your instruction or initiation call carries the settlement_id, so there is nothing to wait for.
Three consequences worth designing around:
- Intermediate states are not skipped. A single engine run that walks
INSTRUCTEDtoCOMPLIANCE_CHECKINGtoCOMPLIANCE_CLEAREDtoAWAITING_DEPOSITSemits four events, not one. Order is guaranteed by the sequence of transitions, but delivery order is not: the delivery worker sends independently, and retries can reorder. Usedata.staterather than arrival order to decide where a settlement is. - The source of a transition makes no difference. Engine walks, on-chain event application, compliance decisions, operator actions, rollbacks and timeouts all emit identically.
SETTLED,ROLLED_BACKandTIMED_OUTreach you as webhooks like any other state. - Subscribers are matched at enqueue time. The endpoints registered when the transition commits are the ones that receive it. An endpoint added a second later does not get that event.
Events you will receive
settlement.state.compliance_checking
Fired when the settlement enters screening. This is the first event you receive for a settlement: the preceding INSTRUCTED state is set at creation rather than by a transition, and emits nothing.
Every settlement that proceeds past creation passes through this state, so you receive this event on the happy path as well as when a party is flagged. Where screening flags a party, the settlement parks here awaiting a manual compliance decision and the next event does not follow immediately.
settlement.state.awaiting_deposits
Fired when compliance has passed and the settlement is registered on-chain, ready for escrow deposits.
settlement.state.rejected
Fired when compliance screening fails outright and the settlement is auto-rejected before any deposits. Nothing is locked on-chain at this point. (A settlement rejected by a manual compliance decision reaches the same REJECTED state - poll or check the settlement after submitting a decision.)
settlement.state.compliance_cleared
Fired when every party has cleared screening, before on-chain registration is attempted. You receive this on the happy path, normally moments before awaiting_deposits.
settlement.state.settled
Fired when the contract has executed on-chain and KeyStone has observed the SettlementExecuted event. This is the delivery signal: at this point every leg has paid out to its recorded recipient and the transfer is irreversible.
settlement.state.finalized
Fired when the settlement has executed on-chain and the record is finalized. This is the terminal success state.
settlement.state.manual_review
Fired when a settlement is parked in MANUAL_REVIEW for operator attention: an engine action failed after compliance cleared, the settlement went stale past its deadline, or an operator escalated it. You will not see this on the happy path; it indicates a settlement needs manual intervention.
settlement.state.rolled_back
Fired when the settlement was aborted on-chain and KeyStone has observed the SettlementAborted event. No leg pays out. Every deposited leg becomes claimable by the address that deposited it.
settlement.state.timed_out
Fired when the settlement passed its deadline without executing, and a timeout claim moved it to a terminal state on-chain. That covers both the unfunded case and a fully funded settlement whose compliance gate never cleared in time. Refunds work exactly as for rolled_back.
test.ping
Fired when you test a webhook endpoint via the Dashboard or the API. Used to verify your endpoint is reachable and correctly verifying signatures.
Complete event list
Thesettlement.state.<state> namespace covers every state in the settlement state machine. Every state reached by a transition is delivered; INSTRUCTED is set at creation and is the one state with no event.
Event filtering
When registering a webhook endpoint, you specify which events to receive using glob-style patterns.
You can subscribe to multiple patterns per endpoint:
The subscription field is
events. Patterns are matched with glob semantics, and a pattern that matches no real event is accepted but simply never fires. If you omit events, it defaults to ["*"]. Subscribing to settlement.state.* is the safe explicit default: you receive every event that fires today and automatically pick up any states that gain webhooks later.An endpoint accepts at most 50 patterns, each at most 100 characters. Both ceilings sit well above the event namespace on this page, so they only bite on generated pattern lists.Signature verification
Every delivery includes anX-Keystone-Signature header carrying a timestamped HMAC-SHA256 signature (t=<unix_seconds>,v1=<hex digest>, signed over "{t}." + raw body) and an X-Keystone-Delivery-Id header for deduplicating retries and replays.
See Verifying signatures in the Webhooks guide for the full scheme, verification snippets in Python and Node.js, the replay-tolerance window, and rotation-grace handling - that section is the single source for signature mechanics.
Secret rotation
When you rotate a webhook secret, KeyStone provides a 24-hour grace period where both the old and new secrets are valid. During this window:X-Keystone-Signatureis signed with the new secretX-Keystone-Signature-Previousis signed with the old secret
X-Keystone-Signature is sent.
Retry behavior
Deliveries are at-least-once: a failed attempt (anything but a2xx, including a 3xx, or a timeout) is retried on a growing backoff ladder (1m, 5m, 30m, 2h, 6h, 12h, 24h) and marked dead after 8 total attempts. Every attempt is visible in the delivery logs, and dead or already-delivered events can be re-sent with the replay endpoints - deduplicate on X-Keystone-Delivery-Id, which stays stable across retries and replays.
Idempotency
The same event may be delivered more than once. Your webhook handler must be idempotent. Recommended patterns:- Check current state before acting. If you receive
settlement.state.finalizedbut the trade is already marked as settled in your system, skip processing. - Use the settlement ID plus state as a deduplication key. Track which settlement/state combinations you have already processed.
- Make downstream calls idempotent. If your handler triggers a transfer or notification, ensure the downstream system also handles duplicates.
Full handler example
A complete webhook handler in TypeScript that covers signature verification, idempotency, and event routing:For SDK-based webhook handling with built-in signature verification, see the TypeScript SDK webhooks guide or Python SDK webhooks guide.