Skip to main content
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);
FieldTypeRequiredDefaultDescription
urlstringYes-HTTPS endpoint URL
descriptionstringNo-Human-readable label
eventsstring[]No["*"]Event patterns to subscribe to
secretstringNoauto-generatedCustom signing secret

Event patterns

PatternMatches
*All events
settlement.*All settlement events
settlement.state.*Specific event
settlement.state.finalizedSettlement completed successfully
settlement.state.rolled_backSettlement rolled back
compliance.*All compliance events
test.pingTest 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

FieldTypeDescription
idstringEndpoint UUID
urlstringDelivery URL
descriptionstring | nullLabel
eventsstring[]Subscribed event patterns
isActivebooleanWhether delivery is enabled
createdAtstringCreation timestamp
updatedAtstringLast update timestamp

update

await client.webhooks.update("endpoint-uuid", {
  url: "https://new-url.com/webhooks",
  events: ["*"],
  isActive: true,
});
FieldTypeDescription
urlstringNew delivery URL
descriptionstring | nullNew label
eventsstring[]New event patterns
isActivebooleanEnable 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

FieldTypeDescription
idstringDelivery UUID
endpointIdstringEndpoint UUID
eventstringEvent name
responseStatusnumber | nullHTTP response status
durationMsnumber | nullRequest duration
successbooleanWhether delivery succeeded
errorstring | nullError message if failed
createdAtstringDelivery 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.