Skip to main content
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"
  }
}
FieldTypeDescription
eventstringDot-delimited event name (e.g. settlement.state.finalized)
data.settlement_idstring (UUID)The settlement this event relates to
data.statestringCurrent settlement state after the transition
data.previous_statestringSettlement state before the transition (absent on instructed)
data.settlement_typestringsingle_platform or cross_platform
data.timestampstring (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.
PatternWhat 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.finalizedOnly 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:
AttemptDelay
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:
  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 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.
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}`);
  }
});
For SDK-based webhook handling with built-in signature verification, see the TypeScript SDK webhooks guide or Python SDK webhooks guide.