This page is a complete reference for every webhook event 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:
{
"event": "settlement.state.awaiting_deposits",
"data": {
"settlement_id": "550e8400-e29b-41d4-a716-446655440000",
"state": "AWAITING_DEPOSITS",
"previous_state": "COMPLIANCE_CLEARED",
"settlement_type": "single_platform",
"timestamp": "2026-03-28T14:32:07Z"
}
}
| Field | Type | Description |
|---|
event | string | Dot-delimited event name (e.g. settlement.state.finalized) |
data.settlement_id | string (UUID) | The settlement this event relates to |
data.state | string | Current settlement state after the transition |
data.previous_state | string | Settlement state before the transition (absent on instructed) |
data.settlement_type | string | single_platform or cross_platform |
data.timestamp | string (ISO 8601) | When the transition occurred |
Settlement state events
These events fire on every settlement state transition. Each event name maps to a state in the settlement state machine.
settlement.state.instructed
Fired when a new settlement is created from matched instructions.
{
"event": "settlement.state.instructed",
"data": {
"settlement_id": "550e8400-e29b-41d4-a716-446655440000",
"state": "INSTRUCTED",
"settlement_type": "single_platform",
"timestamp": "2026-03-28T14:30:00Z"
}
}
What to do: Log the settlement in your OMS. No action required yet - KeyStone automatically begins compliance screening.
settlement.state.compliance_checking
Fired when KeyStone begins screening the parties involved in the settlement.
{
"event": "settlement.state.compliance_checking",
"data": {
"settlement_id": "550e8400-e29b-41d4-a716-446655440000",
"state": "COMPLIANCE_CHECKING",
"previous_state": "INSTRUCTED",
"settlement_type": "single_platform",
"timestamp": "2026-03-28T14:30:02Z"
}
}
What to do: Informational. You may update your UI to show the settlement is in compliance review. No platform action needed.
settlement.state.compliance_cleared
Fired when all parties pass compliance screening (KYC/AML/sanctions via LSEG World-Check and on-chain wallet risk via CipherOwl).
{
"event": "settlement.state.compliance_cleared",
"data": {
"settlement_id": "550e8400-e29b-41d4-a716-446655440000",
"state": "COMPLIANCE_CLEARED",
"previous_state": "COMPLIANCE_CHECKING",
"settlement_type": "single_platform",
"timestamp": "2026-03-28T14:30:18Z"
}
}
What to do: Informational. The settlement will automatically advance to AWAITING_DEPOSITS. You can use this event to pre-stage deposit workflows.
settlement.state.awaiting_deposits
Fired when the on-chain escrow is ready to receive deposits from both parties.
{
"event": "settlement.state.awaiting_deposits",
"data": {
"settlement_id": "550e8400-e29b-41d4-a716-446655440000",
"state": "AWAITING_DEPOSITS",
"previous_state": "COMPLIANCE_CLEARED",
"settlement_type": "single_platform",
"timestamp": "2026-03-28T14:30:20Z"
}
}
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.executing_swap
Fired when both parties have deposited and the atomic swap is executing on-chain.
{
"event": "settlement.state.executing_swap",
"data": {
"settlement_id": "550e8400-e29b-41d4-a716-446655440000",
"state": "EXECUTING_SWAP",
"previous_state": "AWAITING_DEPOSITS",
"settlement_type": "single_platform",
"timestamp": "2026-03-28T14:35:42Z"
}
}
What to do: Informational. The swap is in progress on-chain. No action needed - this state resolves automatically to either finalized or rolled_back.
settlement.state.finalized
Fired when the settlement completes successfully. Assets have been delivered to the receiving parties.
{
"event": "settlement.state.finalized",
"data": {
"settlement_id": "550e8400-e29b-41d4-a716-446655440000",
"state": "FINALIZED",
"previous_state": "EXECUTING_SWAP",
"settlement_type": "single_platform",
"timestamp": "2026-03-28T14:35:48Z"
}
}
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.rolled_back
Fired when a settlement is rolled back. All escrowed assets are returned to the original depositors.
{
"event": "settlement.state.rolled_back",
"data": {
"settlement_id": "550e8400-e29b-41d4-a716-446655440000",
"state": "ROLLED_BACK",
"previous_state": "AWAITING_DEPOSITS",
"settlement_type": "single_platform",
"timestamp": "2026-03-28T15:02:11Z"
}
}
What to do: Return any locked collateral on your side. Update the trade status to failed/cancelled. Notify both parties that the settlement did not complete and their deposits have been returned. This is a terminal state.
settlement.state.timed_out
Fired when a settlement exceeds its timeout window without completing. Escrowed assets are returned automatically.
{
"event": "settlement.state.timed_out",
"data": {
"settlement_id": "550e8400-e29b-41d4-a716-446655440000",
"state": "TIMED_OUT",
"previous_state": "AWAITING_DEPOSITS",
"settlement_type": "single_platform",
"timestamp": "2026-03-28T16:30:00Z"
}
}
What to do: Handle the same as rolled_back. Deposits are returned automatically. Consider re-initiating the settlement if the timeout was due to a transient issue (e.g. delayed deposit). This is a terminal state.
Compliance events
These events provide granular compliance screening results. They fire independently of state transition events.
settlement.compliance.cleared
Fired when all parties in a settlement pass compliance screening.
{
"event": "settlement.compliance.cleared",
"data": {
"settlement_id": "550e8400-e29b-41d4-a716-446655440000",
"state": "COMPLIANCE_CLEARED",
"previous_state": "COMPLIANCE_CHECKING",
"settlement_type": "single_platform",
"timestamp": "2026-03-28T14:30:18Z"
}
}
What to do: Informational. The settlement proceeds automatically. Use this if you want to log compliance clearance separately from state transitions.
settlement.compliance.failed
Fired when a party fails compliance screening (sanctions hit, high-risk wallet, etc.). The settlement will be rolled back.
{
"event": "settlement.compliance.failed",
"data": {
"settlement_id": "550e8400-e29b-41d4-a716-446655440000",
"state": "ROLLED_BACK",
"previous_state": "COMPLIANCE_CHECKING",
"settlement_type": "single_platform",
"timestamp": "2026-03-28T14:30:15Z"
}
}
What to do: Notify the involved parties that the settlement cannot proceed due to compliance. Do not expose the specific compliance failure reason to end users. Log internally for your compliance team to review.
settlement.compliance.flagged
Fired when a party is flagged for manual compliance review. The settlement pauses until a compliance decision is made via the API.
{
"event": "settlement.compliance.flagged",
"data": {
"settlement_id": "550e8400-e29b-41d4-a716-446655440000",
"state": "COMPLIANCE_CHECKING",
"previous_state": "COMPLIANCE_CHECKING",
"settlement_type": "single_platform",
"timestamp": "2026-03-28T14:30:12Z"
}
}
What to do: Route this to your compliance team for manual review. Use the compliance decision endpoint to approve or reject the flagged party. The settlement remains paused until a decision is submitted.
Test event
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.
{
"event": "test.ping",
"data": {
"settlement_id": "00000000-0000-0000-0000-000000000000",
"state": "TEST",
"settlement_type": "test",
"timestamp": "2026-03-28T14:00:00Z"
}
}
What to do: Return a 200 response. Use this to confirm your endpoint is correctly configured and verifying signatures.
Event filtering
When registering a webhook endpoint, you specify which events to receive using glob-style patterns.
| Pattern | What it matches |
|---|
* | All events (settlement state, compliance, test) |
settlement.* | All settlement events (state transitions and compliance) |
settlement.state.* | Only state transition events |
settlement.compliance.* | Only compliance events |
settlement.state.finalized | Only the finalized event |
test.* | Only test events |
You can subscribe to multiple patterns per endpoint:
{
"url": "https://your-platform.com/webhooks/keystone",
"event_types": [
"settlement.state.finalized",
"settlement.state.rolled_back",
"settlement.state.timed_out",
"settlement.compliance.failed"
]
}
Subscribing to only the events you need reduces noise and processing overhead. For most platforms, subscribing to terminal states (finalized, rolled_back, timed_out) plus awaiting_deposits is sufficient.
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.
import crypto from "crypto";
import type { Request, Response } from "express";
function verifyWebhookSignature(
body: string,
signature: string,
secret: string,
): boolean {
const expected = crypto
.createHmac("sha256", secret)
.update(body)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
}
app.post("/webhooks/keystone", (req: Request, res: Response) => {
const signature = req.headers["x-keystone-signature"] as string;
const raw = JSON.stringify(req.body);
if (!verifyWebhookSignature(raw, signature, process.env.WEBHOOK_SECRET!)) {
return res.status(401).send("Invalid signature");
}
// Process the event
handleEvent(req.body);
res.status(200).send("ok");
});
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:
function verifyWithRotation(
body: string,
headers: Record<string, string>,
currentSecret: string,
previousSecret?: string,
): boolean {
const primarySig = headers["x-keystone-signature"];
if (verifyWebhookSignature(body, primarySig, currentSecret)) {
return true;
}
// During rotation, fall back to the previous secret
const previousSig = headers["x-keystone-signature-previous"];
if (previousSecret && previousSig) {
return verifyWebhookSignature(body, previousSig, previousSecret);
}
return false;
}
After 24 hours, the old secret is discarded and only X-Keystone-Signature is sent.
Retry behavior
Failed deliveries (non-2xx response or connection timeout) are retried up to 3 times with exponential backoff:
| Attempt | Delay |
|---|
| 1st retry | ~30 seconds |
| 2nd retry | ~2 minutes |
| 3rd retry | ~10 minutes |
After all retries are exhausted, the delivery is marked as failed in the delivery log. You can inspect failed deliveries in the Dashboard or via the delivery log API.
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 due to retries, network issues, or internal redelivery. Your webhook handler must be idempotent.
Recommended patterns:
- Check current state before acting. If you receive
settlement.state.finalized but the trade is already marked as settled in your system, skip processing.
- Use the settlement ID 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.
app.post("/webhooks/keystone", async (req: Request, res: Response) => {
const { event, data } = req.body;
// Deduplicate: check if we already processed this state for this settlement
const key = `${data.settlement_id}:${data.state}`;
const alreadyProcessed = await redis.get(`webhook:processed:${key}`);
if (alreadyProcessed) {
return res.status(200).send("ok"); // Already handled, return success
}
// Process the event
await handleEvent(event, data);
// Mark as processed (TTL of 7 days)
await redis.set(`webhook:processed:${key}`, "1", "EX", 604800);
res.status(200).send("ok");
});
Full handler example
A complete webhook handler in TypeScript that covers signature verification, idempotency, and event routing:
import crypto from "crypto";
import type { Request, Response } from "express";
const WEBHOOK_SECRET = process.env.KEYSTONE_WEBHOOK_SECRET!;
function verifySignature(body: string, signature: string): boolean {
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(body)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected),
);
}
app.post("/webhooks/keystone", async (req: Request, res: Response) => {
// 1. Verify signature
const signature = req.headers["x-keystone-signature"] as string;
if (!signature || !verifySignature(JSON.stringify(req.body), signature)) {
return res.status(401).send("Invalid signature");
}
// 2. Return 200 immediately
res.status(200).send("ok");
// 3. Process asynchronously
const { event, data } = req.body;
switch (event) {
case "settlement.state.awaiting_deposits":
await triggerDepositWorkflow(data.settlement_id);
break;
case "settlement.state.finalized":
await markTradeAsSettled(data.settlement_id);
await notifyTrader(data.settlement_id, "Your settlement is complete.");
break;
case "settlement.state.rolled_back":
case "settlement.state.timed_out":
await markTradeAsFailed(data.settlement_id);
await notifyTrader(data.settlement_id, "Settlement did not complete.");
break;
case "settlement.compliance.failed":
await escalateToCompliance(data.settlement_id);
break;
case "settlement.compliance.flagged":
await routeToManualReview(data.settlement_id);
break;
case "test.ping":
console.log("Webhook test received");
break;
default:
console.log(`Unhandled event: ${event}`);
}
});