Skip to main content
This page is a complete reference for the webhook events that KeyStone dispatches. For setup instructions, signature verification, and best practices, see the Webhooks guide.

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, named settlement.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 INSTRUCTED to COMPLIANCE_CHECKING to COMPLIANCE_CLEARED to AWAITING_DEPOSITS emits 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. Use data.state rather 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_BACK and TIMED_OUT reach 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.
Deliveries are enqueued once per matching endpoint. Where two platforms share a settlement, each receives the event against its own endpoints, and an endpoint subscribed under more than one environment is still enqueued only once per transition.

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.
What to do: Route the settlement to your compliance team. Submit the outcome with POST /v1/settlements/{settlement_id}/compliance-decision - approving lets the settlement continue (you will receive awaiting_deposits when it registers on-chain); rejecting ends it in REJECTED.

settlement.state.awaiting_deposits

Fired when compliance has passed and the settlement is registered on-chain, ready for escrow deposits.
What to do: This is the key action event. Trigger your deposit workflow - instruct your custody provider (e.g. Fireblocks) to send assets to the escrow address, or notify the trader that a deposit is required.

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.)
What to do: Notify the involved parties that the settlement cannot proceed. Do not expose the specific compliance failure reason to end users - log it internally for your compliance team. This is a terminal state.

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.
What to do: Informational. If awaiting_deposits does not follow, on-chain registration is being retried; a settlement that stays here is one to investigate.

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.
What to do: This is the event to act on for delivery. finalized follows once close-out bookkeeping completes and adds no new economic information.

settlement.state.finalized

Fired when the settlement has executed on-chain and the record is finalized. This is the terminal success state.
What to do: Update your OMS to mark the trade as settled. Notify the trader that their assets have been delivered. This is a terminal 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.
What to do: Alert your operations team. The settlement is parked and an operator resolves it (which drives it to ROLLED_BACK or TIMED_OUT); poll GET /v1/settlements/{id} to follow the outcome.

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.
What to do: Release the trade in your OMS and start your refund flow. Each funded leg is refunded by its own call, and the funds always return to the address that deposited them. See escrow deposits.

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.
What to do: Same as rolled_back. If your side was the one that funded, claim your refund; if it was not, no action is needed on-chain.

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.
What to do: Return a 200 response. Note the data object carries no settlement fields - handle test.ping before any settlement-specific parsing.

Complete event list

The settlement.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.
Subscribe to settlement.state.* unless you have a reason not to. Filtering to a narrow set means a state you did not anticipate passes silently, and the states worth reacting to hardest are the failure terminals.

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 an X-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.
Always use constant-time comparison (crypto.timingSafeEqual in Node.js, hmac.compare_digest in Python) to prevent timing attacks. Never compare signatures with === or ==.

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-Signature is signed with the new secret
  • X-Keystone-Signature-Previous is signed with the old secret
Your verification logic should accept a delivery when EITHER header verifies (both use the timestamped scheme from the Webhooks guide, the previous header signed with the old secret). After 24 hours, the old secret is discarded and only X-Keystone-Signature is sent.

Retry behavior

Deliveries are at-least-once: a failed attempt (anything but a 2xx, 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.
Your endpoint must respond within 10 seconds. If processing takes longer, return 200 immediately and handle the event asynchronously in a background job.

Idempotency

The same event may be delivered more than once. Your webhook handler must be idempotent. Recommended patterns:
  1. Check current state before acting. If you receive settlement.state.finalized but the trade is already marked as settled in your system, skip processing.
  2. Use the settlement ID plus state as a deduplication key. Track which settlement/state combinations you have already processed.
  3. 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.