Webhook

Webhook sends a GAMEMONITORING event to your system after an action on the platform. It is a regular POST request with a JSON body, so a website, panel, or game service can react automatically.

If you are setting up vote rewards, connect the Webhook with this guide first, then use the dedicated flow: Vote rewards.

Connection

This setup links a GAMEMONITORING project to your handler and provides a signing token for verifying incoming requests.

  1. Open My projects, create a project or select an existing one, then go to Webhook settings.
  2. Create a public HTTPS endpoint that accepts POST with Content-Type: application/json and does not redirect requests.
  3. In Webhook settings, enter the full handler URL, for example https://panel.example.com/gamemonitoring-webhook, and save it.
  4. Copy the signing token from the same block and add it to your handler script.
  5. In the handler, verify signature, process the required event_type, and return a successful 2xx response only after the event has been processed.

For local testing, you can run the handler on your computer and expose it through a public HTTPS URL. Use ngrok or another tunneling service, then enter the generated public URL in Webhook settings.

After setup, send a test Webhook from the interface and check the delivery status. For the example URL, your server must accept POST on /gamemonitoring-webhook.

If the test fails, use the handler response as the first clue: 401 means a signature error, 403 or an HTML verification page usually points to WAF or bot protection, and timeout means the URL is unreachable from the internet or responds too slowly.

Handler Requirements

  • The URL must be reachable from the internet. Local addresses, private networks, and URLs with a login or password are not suitable.
  • HTTPS is recommended for production. HTTP is supported, but it protects data in transit less effectively.
  • The handler must accept the POST method and a JSON body without redirects.
  • Return 2xx only after your system has processed the event. Usually 204 No Content is enough.
  • If the event cannot be processed safely, return an error code. 3xx, 4xx, 5xx, timeout, and connection errors are treated as failed delivery.
  • If you use a firewall, bot protection, or an allowlist, add GAMEMONITORING IP addresses to exceptions.
  • Do not return tokens, stack traces, SQL errors, or other internal details in the response body. The handler response is shown in the interface, so the error text must be safe and understandable.

Event Data

Every Webhook arrives with a JSON body and base fields:

  • event_type — the event that must be processed.
  • event_id — unique event ID. Use it together with event_type for idempotency and duplicate-delivery protection.
  • is_test — marks a test delivery from the interface.
  • signature — signature of the event body.
Webhook event example
{
  "event_id": "9824cabb-2203-437e-9b6c-aba43dde3e4b",
  "event_type": "example.event",
  "is_test": false,
  "signature": "0ac4c97a5d934599dbd78985c4bcbb6926e77b4809d2be56333b1b25f638f064"
}

How to read this example: event_type shows which event must be processed; event_id is needed for idempotency before changing state; is_test: true means a technical delivery check; signature is not business data and is used only to authenticate the request.

Processing logic depends on event_type. For server.vote and project.vote, use the dedicated flow: Vote rewards.

If is_test is true, verify the signature and return 2xx, but do not change balances, issue items, or run production operations.

Handler Response Example

Finish each incoming event with one clear result:

  • 204 No Content — the signature is valid, and the event was processed or safely skipped. Return the same response for a test delivery and for an event that has already been processed.
  • 400 Bad Request — required fields are missing. This means a handler bug or an unexpected request body; do not run business logic.
  • 401 Unauthorized — the signature is invalid. Do not make API requests, do not change the database, and do not issue rewards.
  • 500 Internal Server Error — your database, queue, or internal system is temporarily unavailable. Delivery remains failed and can be retried after the cause is fixed.

For example, if the handler received the event, verified the signature, stored event_type + event_id, and processed the event, it can return 204. If the database is unavailable and the event cannot be stored, return 500 so delivery is not marked successful too early.

Signature Verification

The signature is stored in the signature field. Verify it before any business logic, API request, or database change.

To verify it, take all event body fields except signature, sort keys alphabetically, and build a key=value string joined with &. Boolean values are written as true or false.

Signing string
event_id=9824cabb-2203-437e-9b6c-aba43dde3e4b&event_type=example.event&is_test=false

For the example above, the signing string is built only from event_id, event_type, and is_test. Then calculate HMAC-SHA256 with the signing token from Webhook settings and compare it with signature from the request.

Precomputed signatures in the examples use the demo token paste-webhook-token-here. In your handler, use the token from Webhook settings.

In your handler:

  • build the signing string from sorted keys;
  • calculate HMAC-SHA256 with the signing token;
  • compare the result with signature using a constant-time helper: hash_equals in PHP, timingSafeEqual in Node.js, or compare_digest in Python;
  • return 401 when the signature is invalid.

Step 1. Basic Handler

Start with a handler that can accept any Webhook: it reads JSON, verifies signature, handles a test delivery, validates base fields, and returns 204. At this step, the handler only confirms that delivery is accepted correctly. Add event-specific logic after this base path works.

php
<?php
// Replace this token with the signing token from your GAMEMONITORING webhook settings.
$secret = 'paste-webhook-token-here';

// Read and decode the JSON body sent by GAMEMONITORING.
$event = json_decode(file_get_contents('php://input'), true) ?: [];

// Test deliveries are signed too. Normalize the boolean value to the lowercase
// string used by GAMEMONITORING when the signature is calculated.
$isTest = ($event['is_test'] ?? false) === true;
$signingData = array_replace($event, ['is_test' => $isTest ? 'true' : 'false']);

// Build the exact signing string: all body fields except signature,
// sorted by key and joined as key=value pairs with &.
$fields = array_values(array_filter(array_keys($event), fn($field) => $field !== 'signature'));
sort($fields, SORT_STRING);

// Calculate HMAC-SHA256 with the webhook token from your settings.
$signing = implode('&', array_map(fn($field) => $field . '=' . (string) ($signingData[$field] ?? ''), $fields));
$expected = hash_hmac('sha256', $signing, $secret);
$actual = (string) ($event['signature'] ?? '');

// Reject the request before doing any work when the signature is invalid.
if (!hash_equals($expected, $actual)) {
    http_response_code(401);
    exit;
}

// Test deliveries must not change balance, inventory, roles, or production data.
if ($isTest) {
    http_response_code(204);
    exit;
}

// Real deliveries must include an event type and a stable event id.
$eventType = (string) ($event['event_type'] ?? '');
$eventId = (string) ($event['event_id'] ?? '');

if ($eventType === '' || $eventId === '') {
    http_response_code(400);
    exit;
}

// At this point the webhook is trusted. Add event-specific logic here.
syslog(LOG_INFO, 'Accepted webhook event ' . $eventType . ' #' . $eventId);

// 204 tells GAMEMONITORING that the delivery was accepted successfully.
http_response_code(204);

Step 2. Add Deduplication

Webhook uses at-least-once delivery: the same event can arrive more than once. Before the handler changes your system state, make that operation idempotent by event_type + event_id.

First, create a table that stores the pair event_type and event_id with a unique key. If the record already exists, the event has already been processed.

Processed Webhook events table
CREATE TABLE gamemonitoring_webhooks (
  event_type varchar(64) NOT NULL,
  event_id varchar(100) NOT NULL,
  created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (event_type, event_id)
);

Then extend the basic handler: after signature verification and base field validation, store event_type + event_id and perform state changes in the same transaction.

php
<?php
// Replace this token with the signing token from your GAMEMONITORING webhook settings.
$secret = 'paste-webhook-token-here';

// Read and decode the JSON body sent by GAMEMONITORING.
$event = json_decode(file_get_contents('php://input'), true) ?: [];

// Test deliveries are signed too. Normalize the boolean value to the lowercase
// string used by GAMEMONITORING when the signature is calculated.
$isTest = ($event['is_test'] ?? false) === true;
$signingData = array_replace($event, ['is_test' => $isTest ? 'true' : 'false']);

// Build the exact signing string: all body fields except signature,
// sorted by key and joined as key=value pairs with &.
$fields = array_values(array_filter(array_keys($event), fn($field) => $field !== 'signature'));
sort($fields, SORT_STRING);

// Calculate HMAC-SHA256 with the webhook token from your settings.
$signing = implode('&', array_map(fn($field) => $field . '=' . (string) ($signingData[$field] ?? ''), $fields));
$expected = hash_hmac('sha256', $signing, $secret);
$actual = (string) ($event['signature'] ?? '');

// Reject the request before doing any work when the signature is invalid.
if (!hash_equals($expected, $actual)) {
    http_response_code(401);
    exit;
}

// Test deliveries must not change balance, inventory, roles, or production data.
if ($isTest) {
    http_response_code(204);
    exit;
}

// Real deliveries must include an event type and a stable event id.
$eventType = (string) ($event['event_type'] ?? '');
$eventId = (string) ($event['event_id'] ?? '');

if ($eventType === '' || $eventId === '') {
    http_response_code(400);
    exit;
}

// At this point the webhook is trusted. Deduplicate it before event-specific logic.
$pdo = null;

try {
    // Add your local database connection for deduplication and event-specific work.
    $pdo = new PDO('mysql:host=127.0.0.1;dbname=game;charset=utf8mb4', 'game', 'password', [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    ]);

    // Keep deduplication and the real state change in one transaction.
    // If any step fails, return 500 so the delivery can be retried.
    $pdo->beginTransaction();

    // Store the event once. This requires the table to have a unique key on
    // (event_type, event_id). Duplicate deliveries affect zero rows.
    $deduplicate = $pdo->prepare('INSERT IGNORE INTO gamemonitoring_webhooks (event_type, event_id) VALUES (?, ?)');
    $deduplicate->execute([$eventType, $eventId]);

    // The event was already processed earlier. Return success without changing
    // state again, because duplicate delivery is expected.
    if ($deduplicate->rowCount() === 0) {
        $pdo->commit();
        http_response_code(204);
        exit;
    }

    // Add event-specific database changes here. Keep them after the
    // deduplication insert and inside this same transaction.

    // Commit only after deduplication and event-specific work both succeed.
    $pdo->commit();

    // Log only newly processed real events after the transaction succeeds.
    syslog(LOG_INFO, 'Accepted webhook event ' . $eventType . ' #' . $eventId);

    http_response_code(204);
} catch (Throwable $error) {
    // Roll back partial database work so the event can be retried safely.
    if ($pdo instanceof PDO && $pdo->inTransaction()) {
        $pdo->rollBack();
    }

    // 500 keeps the delivery failed instead of marking unfinished work as done.
    http_response_code(500);
}

Do not use nickname, user ID, or server ID as the deduplication key: one user can trigger different events or repeat an allowed action later. The key must be event_type + event_id.

Example: the handler changed your system state, but the connection broke before GAMEMONITORING received 204. Later the delivery is retried, and the same event arrives again. Your handler must find the already stored event_type + event_id, skip the repeated state change, and return 204.

If the handler temporarily cannot process an event, return an error response. After the cause is fixed, delivery can be retried from the interface when retry is available for that event.

Tests and Resend

A test delivery (is_test: true) checks the URL, signature, and HTTP response of the handler. The handler must follow the same processing path: read JSON, verify signature, recognize is_test, and return a successful 2xx response.

A test event must not change balances, inventory, roles, subscriptions, or other production data. A technical log and a 204 response are enough for a test.

If delivery fails, the interface shows the status, HTTP status code, and handler response. After fixing the cause, a failed delivery can be resent when retry is available for that event.