Webhooks

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 TypeDescriptionTriggered When
user.createdNew user provisionedA new user is created via POST /developer/users or auto-provisioned by POST /developer/auth/code
user.updatedUser profile updatedUser profile fields are changed via the API
deposit.confirmedDeposit completedA payment deposit is confirmed for a mapped user
deposit.failedDeposit failedA payment deposit fails for a mapped user
withdrawal.completedWithdrawal completedA withdrawal is processed for a mapped user
withdrawal.failedWithdrawal failedA withdrawal fails for a mapped user
balance.creditedBalance creditedFunds are added via the Developer API
balance.debitedBalance debitedFunds 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

HeaderDescription
Content-Typeapplication/json
X-Webhook-IdID for this delivery attempt; use the signed body webhookId for deduplication
X-TimestampISO 8601 timestamp of the delivery
X-SignatureHex 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:

AttemptDelay After Previous FailureApproximate Time After First Attempt
230 seconds30 seconds
32 minutes~2.5 minutes
410 minutes~12.5 minutes
51 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.

  • 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
Try the live demo Pricing

Have questions about Poker Engine? We reply fast.