Skip to content

Webhooks

Webhooks let KINGSTONE push real-time event notifications to your server. Instead of polling the API for updates, you register a URL and KINGSTONE sends HTTP POST requests when events occur.

Event Types

KINGSTONE supports four webhook event types:

EventDescriptionWhen It Fires
spin.large_winA player won a prize above a configured thresholdImmediately after the spin
settlement.report_readyA daily settlement report has been generatedDaily at ~02:00 UTC
block.low_reserveA game's outcome queue is running low on pre-computed entriesWhen ready block count drops below minimum
api_key.expiringYour API key is approaching its expiration date30 days, 7 days, and 1 day before expiry

Registering a Webhook

With the SDK

typescript
import { KingstoneClient } from '@kingstone/sdk';

const client = new KingstoneClient({
  apiKey: 'ks_sandbox_your_key_here',
  sandbox: true,
});

const webhook = await client.registerWebhook({
  url: 'https://your-server.com/webhooks/kingstone',
  events: ['spin.large_win', 'settlement.report_ready'],
});

console.log(`Webhook ID: ${webhook.id}`);
console.log(`URL: ${webhook.url}`);
console.log(`Events: ${webhook.events.join(', ')}`);

// IMPORTANT: Save this secret — it is only shown once at registration time.
console.log(`Secret: ${webhook.secret}`);

With curl

bash
curl -X POST https://sandbox.kingstone.dev/api/partner/v1/webhooks \
  -H "X-API-Key: ks_sandbox_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-server.com/webhooks/kingstone",
    "events": ["spin.large_win", "settlement.report_ready"]
  }'

The response includes a secret field. Save this immediately — it is only shown once. You need it to verify webhook signatures.

Signature Verification

Every webhook delivery includes an HMAC-SHA256 signature in the X-Kingstone-Signature header. You must verify this signature before processing the event to ensure the request actually came from KINGSTONE.

The signature format is:

X-Kingstone-Signature: sha256=<hex-digest>

Verification Algorithm

  1. Read the raw request body as a string (do NOT parse it as JSON first).
  2. Compute the HMAC-SHA256 digest of the raw body using your webhook secret.
  3. Compare the computed digest to the one in the header using a timing-safe comparison.

Node.js Verification Example

typescript
import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyWebhookSignature(
  rawBody: string,
  signatureHeader: string,
  secret: string,
): boolean {
  const prefix = 'sha256=';
  if (!signatureHeader.startsWith(prefix)) {
    return false;
  }

  const receivedDigest = signatureHeader.slice(prefix.length);
  const computedDigest = createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  // Use timing-safe comparison to prevent timing attacks
  if (receivedDigest.length !== computedDigest.length) {
    return false;
  }

  const a = Buffer.from(receivedDigest, 'hex');
  const b = Buffer.from(computedDigest, 'hex');

  if (a.length !== b.length || a.length === 0) {
    return false;
  }

  return timingSafeEqual(a, b);
}

Express Route Setup

The most common mistake is parsing the body as JSON before verifying the signature. You must use raw body parsing on the webhook route:

typescript
import express from 'express';

const app = express();

// Use raw body parsing for the webhook route
app.post(
  '/webhooks/kingstone',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const rawBody = req.body.toString();
    const signature = req.headers['x-kingstone-signature'] as string;
    const event = req.headers['x-kingstone-event'] as string;

    if (!verifyWebhookSignature(rawBody, signature, WEBHOOK_SECRET)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    const payload = JSON.parse(rawBody);

    switch (event) {
      case 'spin.large_win':
        // Notify player, update leaderboard, etc.
        break;
      case 'settlement.report_ready':
        // Trigger your reconciliation pipeline
        break;
      case 'block.low_reserve':
        // Alert operations team
        break;
      case 'api_key.expiring':
        // Alert engineering team to rotate the key
        break;
    }

    res.status(200).json({ received: true });
  },
);

Delivery Headers

Every webhook POST includes these headers:

HeaderDescription
X-Kingstone-SignatureHMAC-SHA256 signature: sha256=<hex-digest>
X-Kingstone-EventEvent type (e.g., spin.large_win)
X-Kingstone-Delivery-IdUnique delivery attempt identifier
Content-TypeAlways application/json

Retry Policy

If your server does not respond with a 2xx status code, KINGSTONE retries the delivery:

  • 5 attempts total (1 initial + 4 retries)
  • Exponential backoff: 1 minute, 5 minutes, 15 minutes, 1 hour
  • After all attempts are exhausted, the delivery is marked as failed.

You can check delivery history for any webhook:

typescript
const { deliveries } = await client.getWebhookDeliveries(webhook.id, {
  page: 1,
  limit: 20,
});

for (const d of deliveries) {
  console.log(`${d.event}: ${d.status} (${d.attempts}/${d.maxAttempts} attempts)`);
}

Managing Webhooks

List all webhooks

typescript
const { webhooks } = await client.listWebhooks();
for (const wh of webhooks) {
  console.log(`${wh.id}: ${wh.url} — ${wh.events.join(', ')} (active: ${wh.isActive})`);
}

Delete a webhook

typescript
await client.deleteWebhook(webhook.id);
console.log('Webhook deleted');

Deleting a webhook is a soft delete — the record is preserved for audit purposes, but no further deliveries will be sent.

Best Practices

  • Respond quickly. Return 200 within a few seconds. If you need to do heavy processing, acknowledge the webhook first and process asynchronously.
  • Handle duplicates. Use the X-Kingstone-Delivery-Id header to deduplicate deliveries in case of retries.
  • Verify every signature. Never process a webhook without verifying the HMAC signature first.
  • Use HTTPS. Webhook URLs must use HTTPS in production. http://localhost is allowed for sandbox testing only.
  • Store the secret securely. Treat your webhook secret like a password — environment variables, not source code.

Next Steps

KINGSTONE by Predigy Inc.