Webhooks allow your platform to receive real-time notifications when events occur on the poker engine. When a user is provisioned or updated, makes a deposit or withdrawal, or has their balance modified through the API, the poker engine sends an HTTP POST to your registered endpoint. Deliveries are HMAC-signed once a webhook secret exists for your partner account (see Verifying Webhook Signatures).
Subscribing to Events
Register a webhook subscription for each event type you want to receive. Each partner can have one subscription per event type.
Subscribe
POST /api/v1/developer/webhooks
Required scope: webhooks:manage
Request body:
{
"eventType": "balance.credited",
"url": "https://your-platform.com/webhooks/poker"
}
Response (201 Created):
{
"subscription": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"partnerId": "6fa459ea-ee8a-3ca4-894e-db77e160355e",
"eventType": "balance.credited",
"url": "https://your-platform.com/webhooks/poker",
"isActive": true,
"createdAt": "2026-02-18T12:00:00.000Z"
}
}
If a subscription already exists for the same event type, it will be updated (upserted) with the new URL and reactivated. The response is 201 either way.
URL rules: in production the URL must use HTTPS, must not contain embedded credentials, and must not target localhost, .localhost or .local hostnames, or private, loopback or reserved IP addresses. Otherwise the API returns 400 INVALID_URL. Hostnames are checked again at delivery time against the addresses they resolve to, and a failing target counts as a failed delivery.
List Subscriptions
GET /api/v1/developer/webhooks
Required scope: webhooks:manage
Response (200 OK):
{
"subscriptions": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"eventType": "balance.credited",
"url": "https://your-platform.com/webhooks/poker",
"isActive": true,
"createdAt": "2026-02-18T12:00:00.000Z",
"stats": {
"totalDeliveries": 142,
"failedDeliveries": 3,
"lastDelivery": "2026-02-18T11:55:00.000Z"
}
}
]
}
Unsubscribe
DELETE /api/v1/developer/webhooks/:subscriptionId
Required scope: webhooks:manage
Response: 204 No Content
Errors: 404 SUBSCRIPTION_NOT_FOUND if the ID doesn’t exist, 403 FORBIDDEN if it belongs to another partner.
Supported Event Types
| Event Type | Description | Triggered When |
|---|---|---|
user.created | New user provisioned | A new user is created via POST /developer/users or auto-provisioned by POST /developer/auth/code |
user.updated | User profile updated | User profile fields are changed via the API |
deposit.confirmed | Deposit completed | A payment deposit is confirmed for a mapped user |
deposit.failed | Deposit failed | A payment deposit fails for a mapped user |
withdrawal.completed | Withdrawal completed | A withdrawal is processed for a mapped user |
withdrawal.failed | Withdrawal failed | A withdrawal fails for a mapped user |
balance.credited | Balance credited | Funds are added via the Developer API |
balance.debited | Balance debited | Funds are removed via the Developer API |
Any other value returns 400 INVALID_EVENT_TYPE. user.* and balance.* events go only to the partner that made the call. deposit.* and withdrawal.* events go to every partner with a mapping for that user.
Webhook Payload Format
Every webhook delivery sends an HTTP POST with a JSON payload in this format:
{
"event": "balance.credited",
"data": {
"userId": "550e8400-e29b-41d4-a716-446655440000",
"externalUserId": "your-platform-user-42",
"amount": "50.00",
"reference": "purchase_12345",
"newBalance": "150",
"partnerId": "partner_xyz"
},
"timestamp": "2026-02-18T12:00:00.000Z",
"webhookId": "d4e5f6a7-b8c9-1234-5678-abcdef012345",
"partnerId": "partner_xyz"
}
HTTP Headers
| Header | Description |
|---|---|
Content-Type | application/json |
X-Webhook-Id | ID for this delivery attempt; use the signed body webhookId for deduplication |
X-Timestamp | ISO 8601 timestamp of the delivery |
X-Signature | Hex HMAC-SHA256 of the raw body. Sent only when a webhook secret exists for your partner account |
Event Data Shapes
user.created
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"externalUserId": "your-user-42",
"username": "john_doe",
"email": "john@example.com",
"partnerId": "partner_xyz"
}
user.updated
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"externalUserId": "your-user-42",
"changedFields": ["firstName", "lastName"],
"partnerId": "partner_xyz"
}
deposit.confirmed
Payment events identify the user by userId only, without externalUserId. currency is whatever the payment provider reported.
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"amount": "100",
"transactionId": "txn_abc123",
"currency": "USD"
}
deposit.failed
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"amount": "100",
"transactionId": "txn_abc123",
"currency": "USD"
}
withdrawal.completed
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"amount": "50",
"transactionId": "txn_def456",
"currency": "USD"
}
withdrawal.failed
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"amount": "50",
"transactionId": "txn_def456",
"currency": "USD"
}
balance.credited
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"externalUserId": "your-user-42",
"amount": "50.00",
"reference": "purchase_12345",
"newBalance": "150",
"partnerId": "partner_xyz"
}
balance.debited
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"externalUserId": "your-user-42",
"amount": "25.00",
"reference": "withdrawal_789",
"newBalance": "125",
"partnerId": "partner_xyz"
}
Verifying Webhook Signatures
Always verify the X-Signature header before processing the payload to ensure it came from the poker engine and was not tampered with.
The header is a plain hex HMAC-SHA256 of the raw JSON request body, with no prefix. The key is your partner’s webhook secret, including its whsec_ prefix.
Deliveries are signed only after a webhook secret has been generated. There is none by default, and until one exists deliveries arrive with no X-Signature header. The platform admin generates it with POST /api/v1/admin/developer/partners/:id/webhook-secret and gets the value back once. Treat an unsigned delivery as invalid in production. Generating a new secret replaces the old one immediately, with no grace period.
Node.js
const crypto = require('crypto');
function verifyWebhookSignature(rawBody, signature, webhookSecret) {
if (!signature) return false; // No secret generated yet, or not from us
const computed = crypto
.createHmac('sha256', webhookSecret)
.update(rawBody)
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
const a = Buffer.from(computed, 'hex');
const b = Buffer.from(signature, 'hex');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
// Express middleware example
const express = require('express');
const app = express();
// IMPORTANT: Use raw body parser for webhook endpoint
app.post('/webhooks/poker', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-signature'];
const rawBody = req.body.toString();
if (!verifyWebhookSignature(rawBody, signature, process.env.WEBHOOK_SECRET)) {
console.error('Invalid webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
const payload = JSON.parse(rawBody);
// Check for duplicate delivery (use the signed body's webhookId)
// if (alreadyProcessed(payload.webhookId)) return res.status(200).send('OK');
console.log(`Received event: ${payload.event}`);
console.log(`Data:`, payload.data);
// Process the event asynchronously
processEvent(payload).catch(err => console.error('Event processing failed:', err));
// Always respond quickly with 2xx
res.status(200).send('OK');
});
async function processEvent(payload) {
switch (payload.event) {
case 'balance.credited':
// Update your records
break;
case 'deposit.confirmed':
// Confirm purchase on your platform
break;
// Handle other events...
}
}
app.listen(3000, () => console.log('Webhook receiver running on port 3000'));
Python
import hmac
import hashlib
import json
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret"
def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
computed = hmac.new(
secret.encode('utf-8'),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(computed, signature)
@app.route('/webhooks/poker', methods=['POST'])
def webhook_handler():
signature = request.headers.get('X-Signature', '')
if not verify_signature(request.data, signature, WEBHOOK_SECRET):
return jsonify({'error': 'Invalid signature'}), 401
payload = request.get_json()
webhook_id = payload.get('webhookId', '')
print(f"Received event: {payload['event']}")
print(f"Data: {payload['data']}")
# Process asynchronously (e.g., queue the event)
# task_queue.enqueue(process_event, payload)
return 'OK', 200
if __name__ == '__main__':
app.run(port=3000)
PHP
<?php
$webhookSecret = getenv('WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
// Verify signature
$computed = hash_hmac('sha256', $rawBody, $webhookSecret);
if (!hash_equals($computed, $signature)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
$payload = json_decode($rawBody, true);
$webhookId = $payload['webhookId'] ?? '';
error_log("Received event: " . $payload['event']);
// Process the event
switch ($payload['event']) {
case 'balance.credited':
// Update your records
break;
case 'deposit.confirmed':
// Confirm purchase on your platform
break;
}
// Always respond with 200
http_response_code(200);
echo 'OK';
Retry Behavior
If your webhook endpoint returns a non-2xx status code (including a redirect, which is not followed), times out (10 seconds), or is unreachable, the delivery will be retried with exponential backoff. There are 5 attempts in total:
| Attempt | Delay After Previous Failure | Approximate Time After First Attempt |
|---|---|---|
| 2 | 30 seconds | 30 seconds |
| 3 | 2 minutes | ~2.5 minutes |
| 4 | 10 minutes | ~12.5 minutes |
| 5 | 1 hour | ~1 hour 12 minutes |
Retries are picked up by a process that runs every 30 seconds, so actual times can run slightly later. After the fifth failed attempt, the delivery is marked as permanently failed. A pending retry is also marked failed if its subscription has been deactivated.
Each retry re-signs the payload with a fresh timestamp in the X-Timestamp header. The signed body webhookId remains stable across retries; the X-Webhook-Id identifies each attempt. The body’s key order can change between attempts, so verify against the raw bytes of each delivery. Deduplicate only after verifying the body signature.
Best Practices
Respond Quickly
Your webhook endpoint should return a 2xx response within 10 seconds. Process the event asynchronously after responding — queue it for background processing rather than handling it inline.
// GOOD: Respond immediately, process in background
app.post('/webhooks', (req, res) => {
res.status(200).send('OK');
processEventAsync(req.body).catch(console.error);
});
// BAD: Long processing before responding
app.post('/webhooks', async (req, res) => {
await updateDatabase(req.body); // Might take too long
await notifyInternalSystem(req.body); // Even longer
res.status(200).send('OK'); // May timeout
});
Handle Duplicates
Use the webhookId field to detect and skip duplicate deliveries. Store processed webhook IDs in a database or cache:
const processedWebhooks = new Set(); // Use Redis in production
app.post('/webhooks', (req, res) => {
const { webhookId } = req.body;
if (processedWebhooks.has(webhookId)) {
return res.status(200).send('Already processed');
}
processedWebhooks.add(webhookId);
// Process the event...
res.status(200).send('OK');
});
Use HTTPS Endpoints
In production, webhook URLs must use HTTPS. HTTP is only allowed outside production. Use a valid SSL certificate — self-signed certificates will cause delivery failures.
Verify Signatures
Always verify the X-Signature header before processing any webhook payload. Never trust the payload content without verification. See the Verifying Webhook Signatures section above.
Monitor Deliveries
Ask the platform admin to check the admin panel’s Webhook Deliveries page if you are missing events. Your own subscription list also reports totalDeliveries, failedDeliveries and lastDelivery for each subscription. High failure rates may indicate endpoint issues.
Related Guides
- Authentication — How HMAC signing works (same concept used for webhook signatures)
- Security Best Practices — Key management and security recommendations
- API Reference — Complete endpoint reference for webhook subscription management
- Errors — Error codes for webhook-related operations