Webhook endpoints can also be managed in the KeyStone Dashboard under Settings > Webhooks - no code required.
create
Create a webhook endpoint. The signing secret is auto-generated and only returned on creation.
const endpoint = await client.webhooks.create({
url: "https://your-app.com/webhooks/keystone",
description: "Production webhook",
events: ["settlement.state.*", "settlement.state.finalized"],
});
// IMPORTANT: store the secret securely - it's only returned once
console.log(endpoint.secret);
| Field | Type | Required | Default | Description |
|---|
url | string | Yes | - | HTTPS endpoint URL |
description | string | No | - | Human-readable label |
events | string[] | No | ["*"] | Event patterns to subscribe to |
secret | string | No | auto-generated | Custom signing secret |
Event patterns
| Pattern | Matches |
|---|
* | All events |
settlement.* | All settlement events |
settlement.state.* | Specific event |
settlement.state.finalized | Settlement completed successfully |
settlement.state.rolled_back | Settlement rolled back |
compliance.* | All compliance events |
test.ping | Test delivery event |
list / get
const { items } = await client.webhooks.list();
const endpoint = await client.webhooks.get("endpoint-uuid");
console.log(endpoint.url);
console.log(endpoint.events);
console.log(endpoint.isActive);
WebhookEndpointRead
| Field | Type | Description |
|---|
id | string | Endpoint UUID |
url | string | Delivery URL |
description | string | null | Label |
events | string[] | Subscribed event patterns |
isActive | boolean | Whether delivery is enabled |
createdAt | string | Creation timestamp |
updatedAt | string | Last update timestamp |
update
await client.webhooks.update("endpoint-uuid", {
url: "https://new-url.com/webhooks",
events: ["*"],
isActive: true,
});
| Field | Type | Description |
|---|
url | string | New delivery URL |
description | string | null | New label |
events | string[] | New event patterns |
isActive | boolean | Enable or disable delivery |
delete
await client.webhooks.delete("endpoint-uuid");
test
Send a test.ping event to verify delivery works.
const result = await client.webhooks.test("endpoint-uuid");
console.log(result.success); // true
console.log(result.statusCode); // 200
console.log(result.durationMs); // 142
console.log(result.error); // null
rotateSecret
Rotate the signing secret. The previous secret stays valid for 24 hours so you can update your handler without downtime.
const result = await client.webhooks.rotateSecret("endpoint-uuid");
console.log(result.newSecret); // "whsec_..."
console.log(result.previousSecretValidUntil); // 24h from now
listDeliveries
View delivery attempt history for an endpoint.
const { items: deliveries } = await client.webhooks.listDeliveries("endpoint-uuid");
for (const d of deliveries) {
console.log(`${d.event}: ${d.success ? "OK" : "FAILED"} (${d.responseStatus}) - ${d.durationMs}ms`);
if (d.error) console.log(` Error: ${d.error}`);
}
WebhookDeliveryLogRead
| Field | Type | Description |
|---|
id | string | Delivery UUID |
endpointId | string | Endpoint UUID |
event | string | Event name |
responseStatus | number | null | HTTP response status |
durationMs | number | null | Request duration |
success | boolean | Whether delivery succeeded |
error | string | null | Error message if failed |
createdAt | string | Delivery timestamp |
verifySignature
Verify a webhook signature locally. This is a synchronous operation - no API call.
import { verifyWebhookSignature } from "@keystoneos/sdk";
app.post("/webhooks/keystone", express.raw({ type: "application/json" }), (req, res) => {
const isValid = verifyWebhookSignature(
req.body,
req.headers["x-keystone-signature"] as string,
process.env.WEBHOOK_SECRET!,
);
if (!isValid) return res.status(401).send("Invalid signature");
const { event, data } = JSON.parse(req.body);
switch (event) {
case "settlement.state.*":
console.log(`${data.settlement_id}: ${data.from_state} -> ${data.to_state}`);
break;
case "settlement.state.finalized":
console.log(`Finalized: ${data.settlement_id}`);
break;
}
res.sendStatus(200);
});
Use express.raw() (not express.json()) so the middleware receives the raw body for signature verification. Parsing the body before verification can change whitespace and break the signature.
The signature is computed as HMAC-SHA256(webhook_secret, raw_body) and sent in the X-Keystone-Signature header as a hex string. During secret rotation, the previous secret’s signature is sent in X-Keystone-Signature-Previous.