> ## Documentation Index
> Fetch the complete documentation index at: https://docs.keystoneos.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications for settlement events.

Webhooks push real-time event notifications to your server as settlements progress. This is the recommended way to track settlement progress instead of polling for most states.

## How webhooks work

KeyStone's settlement engine dispatches webhooks to all platforms involved in a settlement: 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. Intermediate states passed through in the same run are skipped, and states written directly by on-chain events or operator actions do not emit their own event - see the [event catalog](/guides/webhook-events) for exactly what fires and how to track the rest.

## Local development

During development, your server runs on `localhost` which KeyStone can't reach. The [KeyStone CLI](/sdks/cli) solves this by forwarding webhooks to your local server:

```bash theme={null}
keystone listen --forward-to http://localhost:3000/webhooks/keystone
```

This creates a temporary endpoint, polls for events, and forwards them to your local server in real-time. No ngrok or tunneling tools needed. See the [CLI documentation](/sdks/cli#webhook-forwarding) for details.

## Setting up a webhook endpoint

### 1. Register the endpoint

Create a webhook endpoint in the [KeyStone Dashboard](https://app.keystoneos.xyz) under **Settings > Webhooks**. You need to provide:

* **URL** - Your HTTPS endpoint that will receive webhook deliveries
* **Event types** - Which events to subscribe to (see patterns below)

The webhook secret is displayed once at creation time. Store it securely - you need it to verify webhook signatures.

You can also register endpoints via the API:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.keystoneos.xyz/v1/platforms/me/webhooks \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "url": "https://your-platform.com/webhooks/keystone",
      "event_types": ["settlement.*"]
    }'
  ```

  ```typescript TypeScript SDK theme={null}
  const webhook = await client.webhooks.create({
    url: "https://your-platform.com/webhooks/keystone",
    eventTypes: ["settlement.*"],
  });

  // Store the secret securely - shown only once
  console.log(webhook.secret);
  ```

  ```python Python SDK theme={null}
  webhook = await client.webhooks.create({
      "url": "https://your-platform.com/webhooks/keystone",
      "event_types": ["settlement.*"],
  })

  # Store the secret securely - shown only once
  print(webhook.secret)
  ```
</CodeGroup>

### 2. Event type patterns

| Pattern                      | Matches                  |
| ---------------------------- | ------------------------ |
| `*`                          | All events               |
| `settlement.*`               | All settlement events    |
| `settlement.state.finalized` | Only finalization events |

## Webhook events

Event names are `settlement.state.` followed by the lowercased state name. The events that fire today:

| Event                                  | Trigger                                                                                         |
| -------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `settlement.state.awaiting_deposits`   | Compliance passed and the settlement is registered on-chain - the first event on the happy path |
| `settlement.state.compliance_checking` | Screening flagged a party; the settlement awaits a compliance decision                          |
| `settlement.state.compliance_cleared`  | Rare: the engine halted at on-chain setup and will retry; normally skipped                      |
| `settlement.state.finalized`           | Settlement executed on-chain and finalized - the success event                                  |
| `settlement.state.rejected`            | Compliance screening failed; settlement auto-rejected before any deposits                       |
| `test.ping`                            | Manual test via webhook endpoints API                                                           |

There are no separate `settlement.compliance.*` webhook events - compliance outcomes surface through the state events above. States written directly by on-chain outcomes or operators (`SETTLED`, `ROLLED_BACK`, `TIMED_OUT`, `MANUAL_REVIEW`, and initial `INSTRUCTED`) currently emit no webhook of their own - track those by polling the settlement or its events endpoint; see [states without their own webhook](/guides/webhook-events#states-without-their-own-webhook).

## Webhook payload format

Every webhook delivery includes:

```json theme={null}
{
  "event": "settlement.state.compliance_cleared",
  "data": {
    "settlement_id": "018f6b2a-7c3e-7d90-b1a4-2f9c8d3e5a61",
    "state": "COMPLIANCE_CLEARED"
  }
}
```

The payload is intentionally minimal - it identifies the settlement and its new state. Fetch the full settlement with `GET /v1/settlements/{settlement_id}` when your handler needs more detail.

## Verifying signatures

Every webhook delivery includes an `X-Keystone-Signature` header containing an HMAC-SHA256 hex digest of the request body.

<CodeGroup>
  ```bash cURL theme={null}
  # Verify manually using openssl
  echo -n "$REQUEST_BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET"
  # Compare output with X-Keystone-Signature header
  ```

  ```typescript TypeScript SDK theme={null}
  import { KeystoneClient } from "@keystoneos/sdk";

  app.post("/webhooks/keystone", (req, res) => {
    const signature = req.headers["x-keystone-signature"] as string;
    const payload = JSON.stringify(req.body);

    const isValid = client.webhooks.verifySignature(
      payload,
      signature,
      process.env.WEBHOOK_SECRET!,
    );

    if (!isValid) {
      return res.status(401).send("Invalid signature");
    }

    const event = req.body;
    console.log(`Settlement ${event.data.settlement_id} -> ${event.data.state}`);
    res.status(200).send("ok");
  });
  ```

  ```python Python SDK theme={null}
  from keystoneos import KeystoneClient

  @app.post("/webhooks/keystone")
  async def handle_webhook(request: Request):
      payload = await request.body()
      signature = request.headers.get("x-keystone-signature", "")

      if not KeystoneClient.webhooks.verify_signature(
          payload=payload.decode(),
          signature=signature,
          secret="your-webhook-secret",
      ):
          raise HTTPException(status_code=401, detail="Invalid signature")

      event = await request.json()
      print(f"Settlement {event['data']['settlement_id']} -> {event['data']['state']}")
      return {"status": "ok"}
  ```
</CodeGroup>

<Warning>
  Always use constant-time comparison to prevent timing attacks. Never use `===` or `==` for signature verification.
</Warning>

## Secret rotation

Rotate your webhook secret in the [KeyStone Dashboard](https://app.keystoneos.xyz) under **Settings > Webhooks** by clicking **Rotate Secret** on the endpoint, or via the API:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.keystoneos.xyz/v1/platforms/me/webhooks/$WEBHOOK_ID/rotate-secret \
    -H "Authorization: Bearer $TOKEN"
  ```

  ```typescript TypeScript SDK theme={null}
  const rotated = await client.webhooks.rotateSecret("webhook-id-...");
  console.log(rotated.secret); // New secret
  ```

  ```python Python SDK theme={null}
  rotated = await client.webhooks.rotate_secret("webhook-id-...")
  print(rotated.secret)  # New secret
  ```
</CodeGroup>

During the 24-hour grace period:

* Deliveries include both `X-Keystone-Signature` (new secret) and `X-Keystone-Signature-Previous` (old secret)
* Your server should verify against both headers
* After 24 hours, only the new secret is used

## Testing

Send a test ping from the [KeyStone Dashboard](https://app.keystoneos.xyz) under **Settings > Webhooks**, or via the API:

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.keystoneos.xyz/v1/platforms/me/webhooks/$WEBHOOK_ID/test \
    -H "Authorization: Bearer $TOKEN"
  ```

  ```typescript TypeScript SDK theme={null}
  await client.webhooks.test("webhook-id-...");
  ```

  ```python Python SDK theme={null}
  await client.webhooks.test("webhook-id-...")
  ```
</CodeGroup>

## Best practices

1. **Return 200 quickly** - Process webhooks asynchronously. Return a 200 status immediately and handle the event in a background job.
2. **Handle duplicates** - Webhooks may be delivered more than once. Use the settlement ID plus state to make your handler idempotent.
3. **Verify signatures** - Always verify the HMAC signature before processing. Reject unsigned or invalid requests.
4. **Monitor delivery logs** - Check delivery logs in the [KeyStone Dashboard](https://app.keystoneos.xyz) or via the API:

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.keystoneos.xyz/v1/platforms/me/webhooks/$WEBHOOK_ID/deliveries \
    -H "Authorization: Bearer $TOKEN"
  ```

  ```typescript TypeScript SDK theme={null}
  const deliveries = await client.webhooks.deliveries("webhook-id-...");

  for (const delivery of deliveries.items) {
    console.log(`${delivery.event} - ${delivery.statusCode}`);
  }
  ```

  ```python Python SDK theme={null}
  deliveries = await client.webhooks.deliveries("webhook-id-...")

  for delivery in deliveries.items:
      print(f"{delivery.event} - {delivery.status_code}")
  ```
</CodeGroup>

## Retry policy

Failed deliveries (non-2xx response or timeout) are retried up to 3 times in quick succession with short, jittered backoff (a `Retry-After` header is honored on `429` responses). After that the delivery is marked as failed in the delivery log - there is no delayed redelivery queue, so reconcile missed events by fetching the settlement's current state.

<CardGroup cols={2}>
  <Card title="Webhook Event Catalog" icon="list" href="/guides/webhook-events">
    See every event type with exact payloads and handler examples.
  </Card>

  <Card title="CLI Webhook Forwarding" icon="terminal" href="/sdks/cli#webhook-forwarding">
    Forward webhooks to localhost during development.
  </Card>
</CardGroup>
