Webhook

Webhook wysyła zdarzenie GAMEMONITORING do Twojego systemu po akcji na platformie. To zwykłe żądanie POST z body JSON, dzięki któremu strona, panel lub usługa gry może reagować automatycznie.

Jeśli konfigurujesz nagrody za głosy, najpierw podłącz Webhook według tej instrukcji, a potem użyj osobnego scenariusza: Nagrody za głosy.

Podłączenie

Ta konfiguracja łączy projekt GAMEMONITORING z handlerem i daje token podpisu do weryfikacji przychodzących żądań.

  1. Otwórz Moje projekty, utwórz projekt albo wybierz istniejący, a następnie przejdź do ustawień Webhook.
  2. Utwórz publiczny endpoint HTTPS, który przyjmuje POST z Content-Type: application/json i nie przekierowuje żądań.
  3. W ustawieniach Webhook wpisz pełny URL handlera, na przykład https://panel.example.com/gamemonitoring-webhook, i zapisz go.
  4. Skopiuj token podpisu z tego samego bloku i wpisz go w skrypcie handlera.
  5. W handlerze sprawdź signature, obsłuż potrzebne event_type i zwróć udaną odpowiedź 2xx dopiero po przetworzeniu zdarzenia.

Do lokalnego testowania możesz uruchomić handler na swoim komputerze i wystawić go przez publiczny URL HTTPS. Użyj ngrok albo innej usługi tunelowania, a wygenerowany publiczny URL wpisz w ustawieniach Webhook.

Po konfiguracji wyślij testowy Webhook z interfejsu i sprawdź status dostawy. Dla przykładowego URL serwer musi przyjmować POST na /gamemonitoring-webhook.

Jeśli test się nie uda, zacznij od odpowiedzi handlera: 401 oznacza błąd podpisu, 403 albo strona weryfikacyjna HTML zwykle wskazuje WAF lub bot protection, a timeout oznacza, że URL jest niedostępny z internetu albo odpowiada za wolno.

Wymagania handlera

  • URL musi być dostępny z internetu. Lokalne adresy, sieci prywatne i URL z loginem lub hasłem nie są odpowiednie.
  • HTTPS jest zalecany dla produkcji. HTTP jest obsługiwany, ale słabiej chroni dane w transmisji.
  • Handler musi przyjmować metodę POST i body JSON bez przekierowań.
  • Zwracaj 2xx dopiero po przetworzeniu zdarzenia przez Twój system. Zwykle wystarczy 204 No Content.
  • Jeśli zdarzenia nie da się bezpiecznie przetworzyć, zwróć kod błędu. 3xx, 4xx, 5xx, timeout i błąd połączenia są traktowane jako nieudana dostawa.
  • Jeśli używasz firewalla, bot protection lub allowlist, dodaj adresy IP GAMEMONITORING do wyjątków.
  • Nie zwracaj w treści odpowiedzi tokenów, stack trace, błędów SQL ani innych szczegółów wewnętrznych. Odpowiedź handlera jest widoczna w interfejsie, więc tekst błędu musi być bezpieczny i zrozumiały.

Dane zdarzenia

Każdy Webhook przychodzi z body JSON i podstawowymi polami:

  • event_type — jakie zdarzenie trzeba obsłużyć.
  • event_id — unikalny ID zdarzenia. Użyj go razem z event_type do idempotencji i ochrony przed ponowną dostawą.
  • is_test — oznacza testową dostawę z interfejsu.
  • signature — podpis body zdarzenia.
Przykład zdarzenia Webhook
{
  "event_id": "9824cabb-2203-437e-9b6c-aba43dde3e4b",
  "event_type": "example.event",
  "is_test": false,
  "signature": "0ac4c97a5d934599dbd78985c4bcbb6926e77b4809d2be56333b1b25f638f064"
}

Jak czytać przykład: event_type pokazuje, jakie zdarzenie trzeba obsłużyć; event_id jest potrzebne do idempotencji przed zmianą stanu; is_test: true oznacza techniczne sprawdzenie dostawy; signature nie jest daną biznesową i służy tylko do uwierzytelnienia żądania.

Logika przetwarzania zależy od event_type. Dla server.vote i project.vote użyj osobnego scenariusza: Nagrody za głosy.

Jeśli is_test ma wartość true, sprawdź podpis i zwróć 2xx, ale nie zmieniaj salda, nie wydawaj przedmiotów i nie uruchamiaj operacji produkcyjnych.

Przykład odpowiedzi handlera

Każde przychodzące zdarzenie zakończ jednym jasnym wynikiem:

  • 204 No Content — podpis jest poprawny, a zdarzenie zostało przetworzone albo bezpiecznie pominięte. Tę samą odpowiedź zwracaj dla testowej dostawy i dla zdarzenia już przetworzonego.
  • 400 Bad Request — brakuje wymaganych pól. To oznacza błąd handlera albo nieoczekiwane body żądania; nie uruchamiaj logiki biznesowej.
  • 401 Unauthorized — podpis jest nieprawidłowy. Nie wykonuj żądań API, nie zmieniaj bazy i nie wydawaj nagród.
  • 500 Internal Server Error — baza, kolejka lub wewnętrzny system jest tymczasowo niedostępny. Dostawa pozostanie nieudana i można ją ponowić po usunięciu przyczyny.

Przykład: jeśli handler odebrał zdarzenie, sprawdził podpis, zapisał event_type + event_id i przetworzył zdarzenie, może zwrócić 204. Jeśli baza jest niedostępna i zdarzenia nie da się zapisać, lepiej zwrócić 500, żeby dostawa nie została za wcześnie oznaczona jako udana.

Weryfikacja podpisu

Podpis znajduje się w polu signature. Sprawdzaj go przed logiką biznesową, żądaniami API i zmianami w bazie.

Do weryfikacji weź wszystkie pola body poza signature, posortuj klucze alfabetycznie i zbuduj ciąg key=value połączony przez &. Wartości boolean zapisuj jako true albo false.

Ciąg do podpisu
event_id=9824cabb-2203-437e-9b6c-aba43dde3e4b&event_type=example.event&is_test=false

Dla przykładu powyżej ciąg podpisu buduje się tylko z event_id, event_type i is_test. Następnie oblicz HMAC-SHA256 tokenem podpisu z ustawień Webhook i porównaj wynik z signature z żądania.

Gotowe podpisy w przykładach są policzone dla tokena demonstracyjnego paste-webhook-token-here. W swoim handlerze użyj tokena z ustawień Webhook.

W handlerze:

  • zbuduj ciąg do podpisu z posortowanych kluczy;
  • oblicz HMAC-SHA256 tokenem podpisu;
  • porównaj wynik z signature funkcją o stałym czasie porównania: hash_equals w PHP, timingSafeEqual w Node.js albo compare_digest w Pythonie;
  • zwróć 401, jeśli podpis jest nieprawidłowy.

Krok 1. Podstawowy handler

Zacznij od handlera, który przyjmuje dowolny Webhook: czyta JSON, sprawdza signature, obsługuje testową dostawę, sprawdza podstawowe pola i zwraca 204. Na tym etapie handler tylko potwierdza, że dostawa jest przyjmowana poprawnie. Logikę konkretnego zdarzenia dodaj po tym, gdy ten podstawowy przepływ działa.

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

Krok 2. Dodaj deduplikację

Webhook używa modelu at-least-once: to samo zdarzenie może przyjść więcej niż raz. Zanim handler zmieni stan Twojego systemu, zadbaj o idempotentność tej operacji po event_type + event_id.

Najpierw utwórz tabelę, która zapisuje parę event_type i event_id z unikalnym kluczem. Jeśli rekord już istnieje, zdarzenie było już przetworzone.

Tabela przetworzonych zdarzeń Webhook
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)
);

Następnie rozszerz podstawowy handler: po sprawdzeniu podpisu i podstawowych pól zapisz event_type + event_id, a zmiany stanu wykonuj w tej samej transakcji.

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

Nie używaj nicku, ID użytkownika ani ID serwera jako klucza deduplikacji: jeden użytkownik może wywołać różne zdarzenia albo później powtórzyć dozwoloną akcję. Kluczem musi być event_type + event_id.

Przykład: handler zmienił stan Twojego systemu, ale połączenie zerwało się przed otrzymaniem przez GAMEMONITORING 204. Później dostawa zostaje ponowiona i to samo zdarzenie przychodzi jeszcze raz. Handler musi znaleźć zapisane event_type + event_id, pominąć ponowną zmianę stanu i zwrócić 204.

Jeśli handler tymczasowo nie może przetworzyć zdarzenia, zwróć odpowiedź z błędem. Po usunięciu przyczyny dostawę można ponowić z interfejsu, jeśli ponowienie jest dostępne dla tego zdarzenia.

Testy i ponowna wysyłka

Dostawa testowa (is_test: true) sprawdza URL, podpis i odpowiedź HTTP handlera. Handler musi przejść tę samą ścieżkę przetwarzania: odczytać JSON, sprawdzić signature, rozpoznać is_test i zwrócić udaną odpowiedź 2xx.

Zdarzenie testowe nie powinno zmieniać salda, ekwipunku, ról, subskrypcji ani innych danych produkcyjnych. Do testu wystarczą techniczny log i odpowiedź 204.

Jeśli dostawa zakończy się błędem, interfejs pokazuje status, kod HTTP i odpowiedź handlera. Po naprawieniu przyczyny nieudaną dostawę można wysłać ponownie, jeśli ponowienie jest dostępne dla tego zdarzenia.