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:
| Event | Description | When It Fires |
|---|---|---|
spin.large_win | A player won a prize above a configured threshold | Immediately after the spin |
settlement.report_ready | A daily settlement report has been generated | Daily at ~02:00 UTC |
block.low_reserve | A game's outcome queue is running low on pre-computed entries | When ready block count drops below minimum |
api_key.expiring | Your API key is approaching its expiration date | 30 days, 7 days, and 1 day before expiry |
Registering a Webhook
With the SDK
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
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
- Read the raw request body as a string (do NOT parse it as JSON first).
- Compute the HMAC-SHA256 digest of the raw body using your webhook secret.
- Compare the computed digest to the one in the header using a timing-safe comparison.
Node.js Verification Example
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:
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:
| Header | Description |
|---|---|
X-Kingstone-Signature | HMAC-SHA256 signature: sha256=<hex-digest> |
X-Kingstone-Event | Event type (e.g., spin.large_win) |
X-Kingstone-Delivery-Id | Unique delivery attempt identifier |
Content-Type | Always 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:
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
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
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-Idheader 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://localhostis allowed for sandbox testing only. - Store the secret securely. Treat your webhook secret like a password — environment variables, not source code.
Next Steps
- Learn about compliance and RTP tracking
- Review the error codes reference for webhook-related errors
