Vote Rewards

Vote rewards use the server.vote and project.vote Webhook events: the handler receives the event, fetches vote data through the API, finds the user in your system, and issues the reward only once.

First configure the project Webhook and signature verification. Vote data is requested through GET /votes/:vote_id.

How the Flow Works

  1. Receive the Webhook event and verify signature. If is_test is true, return 204 without fetching the vote and without issuing a reward.
  2. Make sure event_type is server.vote or project.vote.
  3. Use event_id as the vote ID.
  4. Fetch vote data through GET /votes/:vote_id and find the player in your system.
  5. In one transaction, apply duplicate-processing protection by event_type + event_id and issue the reward only for a new event.
  6. If the reward cannot be issued safely, return an error response. After fixing the cause, retry delivery from the interface.

Reward Example

Suppose player PlayerName voted for the server with ID 1, and your system must add 100 coins.

  1. GAMEMONITORING sends a Webhook with event_type: server.vote and event_id: 9824cabb-2203-437e-9b6c-aba43dde3e4b.
  2. The handler verifies signature. If the signature is invalid, it returns 401 and stops.
  3. The handler requests GET /votes/9824cabb-2203-437e-9b6c-aba43dde3e4b, receives nickname, server, and user data, and finds the local account.
  4. In a transaction, the handler stores event_type + event_id for duplicate-processing protection.
  5. For a new event, the handler adds 100 coins in the same transaction.
  6. On repeated delivery, the handler finds the already stored event, does not issue the reward again, and returns 204.

The same flow also works for items, roles, VIP time, promo codes, or work queued in an internal system.

Vote Event

When a server receives a vote, GAMEMONITORING sends server.vote; when a project receives a vote, it sends project.vote. The event body contains only delivery data: event_type, event_id, is_test, and signature. Full vote data must be requested separately.

Event example
{
  "event_id": "9824cabb-2203-437e-9b6c-aba43dde3e4b",
  "event_type": "server.vote",
  "is_test": false,
  "signature": "ae83b8aba88a3a9ab3b97b1f6d65664da5628a9cb64d56d5132807bca5472e4f"
}

In either event, event_id is the vote ID. Do not use the Webhook body as the source for nickname, entity, or user data: those values come from the API.

Fetch Vote Data

Use event_id as vote_id and request the vote data through GET /votes/:vote_id:

Vote data request
curl -sS "https://api.gamemonitoring.net/votes/9824cabb-2203-437e-9b6c-aba43dde3e4b"

For reward delivery, use response.entity_type and response.entity_id to distinguish the target. A server vote also contains response.server; a project vote contains response.project. Both types contain response.nickname and public response.user data.

How to use the fields: response.nickname helps find the account in your database, response.entity_type and response.entity_id select the reward rule, and response.user.id can be stored in the reward log as the GAMEMONITORING user ID that voted. Always verify that server.vote returned entity_type: server and project.vote returned entity_type: project.

If the API is temporarily unavailable or returns an unexpected response, do not issue a reward without verification. Return an error code, fix the cause, and retry delivery from the interface.

Step 3. Vote Reward Handler

The example continues the basic handler: it verifies the signature, fetches vote data, prevents duplicate processing, and applies the reward in one transaction. Replace the user table name, balance field, and player lookup rule with your system structure.

Before running the example, configure the project Webhook, check GET /votes/:vote_id, and replace the SQL user update queries with your account model.

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

// Add the GAMEMONITORING API URL and reward settings for vote events.
$apiUrl = 'https://api.gamemonitoring.net';
$rewardAmount = '1.00';

// 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;
}

// This reward handler processes server and project vote events.
if (!in_array($eventType, ['server.vote', 'project.vote'], true)) {
    http_response_code(204);
    exit;
}

// At this point the webhook is trusted. Load vote data before opening a database transaction.
$pdo = null;

try {
    // Load full vote data by event_id. Nickname, entity, and user data are not
    // in the webhook body. Return 500 if the API cannot confirm the vote.
    $voteUrl = $apiUrl . '/votes/' . rawurlencode($eventId);
    $voteContext = stream_context_create(['http' => ['timeout' => 5]]);
    $voteBody = @file_get_contents($voteUrl, false, $voteContext);

    if ($voteBody === false) {
        throw new RuntimeException('Vote API request failed');
    }

    $voteResponse = json_decode($voteBody, true) ?: [];
    $vote = $voteResponse['response'] ?? null;

    // Do not issue a reward when the vote response is missing a concrete nickname.
    if (!is_array($vote) || !isset($vote['nickname']) || !is_string($vote['nickname'])) {
        throw new RuntimeException('Vote API response does not include nickname');
    }

    // Verify that the API entity matches the event before changing the account.
    $expectedEntityType = $eventType === 'project.vote' ? 'project' : 'server';
    if (($vote['entity_type'] ?? '') !== $expectedEntityType) {
        throw new RuntimeException('Vote entity type does not match event type');
    }

    // Use vote nickname to update the local account. The entity id is available in
    // vote.entity_id and in either vote.server.id or vote.project.id.
    $nickname = trim($vote['nickname']);

    if ($nickname === '') {
        throw new RuntimeException('Vote nickname is empty');
    }

    // 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.
    $balance = $pdo->prepare('UPDATE users SET balance = balance + ? WHERE nickname = ?');
    $balance->execute([$rewardAmount, $nickname]);

    // 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);
}