Webhook

Webhook отправляет событие GAMEMONITORING в вашу систему после действия на платформе. Это обычный POST-запрос с JSON-телом, который позволяет сайту, панели или игровому сервису реагировать автоматически.

Если вы настраиваете награды за голосование, сначала подключите Webhook по этой инструкции, затем используйте отдельный сценарий: Награды за голосование.

Подключение

Эта настройка связывает проект GAMEMONITORING с вашим обработчиком и дает токен подписи для проверки входящих запросов.

  1. Откройте Мои проекты, создайте проект или выберите существующий, затем перейдите в настройки Webhook.
  2. Создайте публичный HTTPS-эндпоинт, который принимает POST с Content-Type: application/json и не перенаправляет запросы.
  3. В настройках Webhook укажите полный URL обработчика, например https://panel.example.com/gamemonitoring-webhook, и сохраните его.
  4. Скопируйте токен подписи из этого же блока и укажите его в скрипте обработчика.
  5. В обработчике проверьте signature, обработайте нужный event_type и верните успешный 2xx-ответ только после обработки события.

Для локального тестирования можно запустить обработчик на своем компьютере и открыть его через публичный HTTPS-URL. Для этого подойдет ngrok или другой сервис туннелирования; сгенерированный публичный URL укажите в настройках Webhook.

После настройки отправьте тестовый Webhook из интерфейса и проверьте статус доставки. Для URL из примера сервер должен принимать POST на путь /gamemonitoring-webhook.

Если тест не прошел, ориентируйтесь на ответ обработчика: 401 означает ошибку подписи, 403 или HTML-страница проверки обычно указывает на WAF или bot protection, а тайм-аут означает, что URL недоступен из интернета или отвечает слишком долго.

Требования к обработчику

  • URL должен быть доступен из интернета. Локальные адреса, приватные сети и URL с логином или паролем не подходят.
  • HTTPS рекомендуется для продакшена. HTTP поддерживается, но слабее защищает данные при передаче.
  • Обработчик должен принимать метод POST и JSON-тело без редиректов.
  • Возвращайте 2xx только после того, как событие обработано вашей системой. Обычно достаточно 204 No Content.
  • Если событие нельзя безопасно обработать, верните код ошибки. 3xx, 4xx, 5xx, тайм-аут и ошибка подключения считаются ошибочной доставкой.
  • Если у вас firewall, bot protection или allowlist, добавьте IP-адреса GAMEMONITORING в исключения.
  • Не возвращайте в теле ответа токены, stack trace, SQL-ошибки и другие внутренние детали. Ответ обработчика отображается в интерфейсе, поэтому текст ошибки должен быть безопасным и понятным.

Данные события

Каждый Webhook приходит с JSON-телом и базовыми полями:

  • event_type — какое событие нужно обработать.
  • event_id — уникальный ID события. Используйте его вместе с event_type для идемпотентности и защиты от повторной доставки.
  • is_test — признак тестовой доставки из интерфейса.
  • signature — подпись тела события.
Пример Webhook-события
{
  "event_id": "9824cabb-2203-437e-9b6c-aba43dde3e4b",
  "event_type": "example.event",
  "is_test": false,
  "signature": "0ac4c97a5d934599dbd78985c4bcbb6926e77b4809d2be56333b1b25f638f064"
}

Как читать пример: event_type показывает, какое событие нужно обработать; event_id нужен для идемпотентности перед изменением состояния; is_test: true означает техническую проверку доставки; signature нельзя использовать в бизнес-логике, она нужна только для проверки подлинности запроса.

Логика обработки зависит от event_type. Для server.vote и project.vote используйте сценарий: Награды за голосование.

Если is_test равен true, проверьте подпись и верните 2xx, но не меняйте баланс, не выдавайте предметы и не запускайте рабочие операции.

Пример ответа обработчика

Для каждого входящего события завершайте обработку одним понятным результатом:

  • 204 No Content — подпись корректна, событие обработано или безопасно пропущено. Такой же ответ возвращайте для тестовой доставки и для уже обработанного события.
  • 400 Bad Request — в событии нет обязательных полей. Это означает ошибку в обработчике или неожиданное тело запроса, бизнес-логику запускать нельзя.
  • 401 Unauthorized — подпись неверная. Не выполняйте API-запросы, не меняйте базу и не выдавайте награды.
  • 500 Internal Server Error — ваша база, очередь или внутренняя система временно недоступна. Доставка останется ошибочной, ее можно повторить после исправления причины.

Например, если обработчик получил событие, проверил подпись, сохранил event_type + event_id и обработал событие, он может вернуть 204. Если база недоступна и событие нельзя сохранить, лучше вернуть 500, чтобы доставка не была отмечена успешной раньше времени.

Проверка подписи

Подпись находится в поле signature. Проверяйте ее до любой бизнес-логики, запросов к API и изменений в базе.

Для проверки возьмите все поля тела события, кроме signature, отсортируйте ключи по алфавиту и соберите строку key=value через &. Булевы значения записываются как true или false.

Строка для подписи
event_id=9824cabb-2203-437e-9b6c-aba43dde3e4b&event_type=example.event&is_test=false

Для примера выше строка подписи собирается только из event_id, event_type и is_test. После этого вычислите HMAC-SHA256 с токеном подписи из настроек Webhook и сравните результат с signature из запроса.

Готовые подписи в примерах рассчитаны для демонстрационного токена paste-webhook-token-here. В своем обработчике используйте токен из настроек Webhook.

В обработчике:

  • соберите строку для подписи из отсортированных ключей;
  • вычислите HMAC-SHA256 с токеном подписи;
  • сравните результат с signature функцией с постоянным временем сравнения: hash_equals в PHP, timingSafeEqual в Node.js или compare_digest в Python;
  • верните 401, если подпись неверная.

Шаг 1. Базовый обработчик

Начните с обработчика, который принимает любой Webhook: читает JSON, проверяет signature, обрабатывает тестовую доставку, проверяет базовые поля и возвращает 204. На этом шаге обработчик только подтверждает, что доставка принимается корректно. Логику конкретного события добавляйте после того, как этот базовый путь работает.

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

Шаг 2. Добавьте дедупликацию

Webhook использует модель at-least-once: одно и то же событие может прийти несколько раз. Перед тем как обработчик меняет состояние вашей системы, сделайте эту операцию идемпотентной по event_type + event_id.

Сначала создайте таблицу, которая хранит пару event_type и event_id с уникальным ключом. Если запись уже существует, событие уже обработано.

Таблица обработанных 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)
);

Затем расширьте базовый обработчик: после проверки подписи и базовых полей сохраните event_type + event_id, а изменения состояния выполняйте в той же транзакции.

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

Не используйте никнейм, ID пользователя или ID сервера как ключ дедупликации: один пользователь может инициировать разные события или повторить разрешенное действие позже. Ключом должен быть именно event_type + event_id.

Пример: обработчик изменил состояние вашей системы, но соединение оборвалось до того, как GAMEMONITORING получил ответ 204. Позже доставку отправляют повторно, и то же событие приходит снова. Обработчик должен увидеть уже сохраненные event_type + event_id, пропустить повторное изменение и вернуть 204.

Если обработчик временно не может обработать событие, верните ответ с ошибкой. После исправления причины доставку можно повторить из интерфейса, если повтор доступен для этого события.

Тесты и повторная отправка

Тестовая доставка (is_test: true) проверяет URL, подпись и HTTP-ответ обработчика. Обработчик должен пройти тот же путь обработки: прочитать JSON, проверить signature, распознать is_test и вернуть успешный 2xx-ответ.

Тестовое событие не должно менять баланс, инвентарь, роли, подписки или другие рабочие данные. Для теста достаточно технического лога и ответа 204.

Если доставка завершилась ошибкой, в интерфейсе отображаются статус, HTTP-код и ответ обработчика. После исправления причины доставку с ошибкой можно отправить повторно, если повтор доступен для этого события.