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

Settlement webhooks are dispatched by KeyStone’s settlement engine: each time an engine run advances a settlement and leaves it in a new state, one event fires, named for the state the run ended in (settlement.state. plus the lowercased state name). Two consequences worth designing around:
  • Intermediate states are skipped. A single engine run can pass through several states; only the final one emits an event. On the happy path, compliance passes and on-chain registration completes in one run, so the first event you receive is awaiting_deposits (not instructed or compliance_cleared).
  • States written outside the engine do not emit their own event. On-chain outcomes (SETTLED, ROLLED_BACK, TIMED_OUT) and operator actions are applied directly and are visible immediately via the API; a webhook follows only where the engine runs afterwards (execution is followed by an engine run that finalizes the settlement and emits finalized). See states without their own webhook below.

Events you will receive

settlement.state.awaiting_deposits

Fired when compliance has passed and the settlement is registered on-chain, ready for escrow deposits. On the happy path this is the first event for a new settlement.
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.compliance_checking

Fired when screening flags a party and the settlement parks in COMPLIANCE_CHECKING awaiting a manual compliance decision. (When screening passes outright, the settlement continues to AWAITING_DEPOSITS in the same engine run and no compliance_checking event fires.)
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.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 only when compliance passed but the engine run halted at COMPLIANCE_CLEARED (for example, on-chain setup needed a retry). Normally you will not see this event - the settlement continues to AWAITING_DEPOSITS in the same run.
What to do: Informational. The engine retries setup and you will receive awaiting_deposits when registration completes.

settlement.state.finalized

Fired when the settlement has executed on-chain and the record is finalized. This is the success event.
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.

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.

States without their own webhook

The settlement.state.<state> namespace covers every state in the settlement state machine, but five states are currently never delivered as webhooks:
Failure outcomes (ROLLED_BACK, TIMED_OUT) currently arrive without a webhook. If your integration must react to failed settlements promptly, poll GET /v1/settlements/{settlement_id} for any settlement that has passed its timeout_at without a finalized 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:
Subscribing to settlement.state.* is the safe default: you receive every event that fires today and automatically pick up any states that gain webhooks later.

Signature verification

Every delivery includes an X-Keystone-Signature header containing an HMAC-SHA256 hex digest of the raw request body, signed with your webhook secret.
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 check both headers during the transition:
After 24 hours, the old secret is discarded and only X-Keystone-Signature is sent.

Retry behavior

Each delivery is attempted with up to 3 retries in quick succession (short, jittered backoff; a Retry-After header is honored on 429 responses). A non-2xx response or a timeout after the final attempt marks the delivery as failed in the delivery log - there is no delayed redelivery queue. Because failed deliveries are not replayed later, treat the delivery log as your recovery tool: inspect failures in the Dashboard or via the delivery log API, and reconcile missed events by fetching the settlement’s current state.
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.