Что получится: вторая система — аналитика, аудит, хранилище переписки — получает копию всех уведомлений канала по своему адресу. Основное приложение при этом продолжает читать очередь или вебхук как раньше: подписка ничего у него не забирает. Приёмник — на PHP, с проверкой подписи и защитой от повторов.
┌──► очередь или вебхук ──► основное приложение (бот, CRM)
события канала ─────┤
└──► подписка «analytics» ──► приёмник на PHP ──► events.db
подпись HMAC, свои повторы
Что понадобится
- канал с ключом и переменные
GRATIO_URL,GRATIO_ID,GRATIO_TOKEN; - PHP 8 с PDO SQLite на сервере с публичным HTTPS-адресом.
Шаг 1. Приёмник
Файл gratio.php. Он проверяет подпись по сырому телу, отсекает повторную доставку по X-Gratio-Delivery и сохраняет уведомление.
<?php
const TOLERANCE = 300; // подпись не старше 5 минут
function gratio_verify(string $secret, string $body, string $header): bool {
$parts = [];
foreach (explode(',', $header) as $p) {
[$k, $v] = array_pad(explode('=', $p, 2), 2, '');
$parts[$k] = $v;
}
$t = (int)($parts['t'] ?? 0);
if (!$t || empty($parts['v1']) || abs(time() - $t) > TOLERANCE) {
return false;
}
return hash_equals(hash_hmac('sha256', $t . '.' . $body, $secret), $parts['v1']);
}
$body = file_get_contents('php://input');
if (!gratio_verify(getenv('GRATIO_SUB_SECRET'), $body, $_SERVER['HTTP_X_GRATIO_SIGNATURE'] ?? '')) {
http_response_code(401);
exit;
}
$db = new PDO('sqlite:' . __DIR__ . '/events.db');
$db->exec('create table if not exists events (
delivery text primary key, instance text, type text, chat_id text, body text, at int)');
$event = json_decode($body, true);
$stmt = $db->prepare('insert or ignore into events values (?, ?, ?, ?, ?, ?)');
$stmt->execute([
$_SERVER['HTTP_X_GRATIO_DELIVERY'] ?? '',
$_SERVER['HTTP_X_GRATIO_INSTANCE'] ?? '',
$event['typeWebhook'] ?? '',
$event['senderData']['chatId'] ?? ($event['chatId'] ?? ''),
$body,
$event['timestamp'] ?? time(),
]);
http_response_code(200);
echo 'ok';
Повтор приходит с тем же X-Gratio-Delivery — insert or ignore его пропустит, а ответ 200 скажет Gratio, что всё принято.
Шаг 2. Создайте подписку
Подписку создаёт ключ канала. Секрет не передаём — Gratio создаст его сам и покажет один раз:
curl
curl -X POST "$GRATIO_URL/waInstance$GRATIO_ID/subscriptions/$GRATIO_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "analytics",
"url": "https://bi.example.ru/gratio.php"
}'
Python
import os
import requests
BASE = f"{os.environ['GRATIO_URL']}/waInstance{os.environ['GRATIO_ID']}"
TOKEN = os.environ["GRATIO_TOKEN"]
url = f"{BASE}/subscriptions/{TOKEN}"
payload = {"name": "analytics", "url": "https://bi.example.ru/gratio.php"}
response = requests.post(url, json=payload, timeout=60)
print(response.status_code, response.json())
Node.js
const { GRATIO_URL, GRATIO_ID, GRATIO_TOKEN } = process.env;
const base = `${GRATIO_URL}/waInstance${GRATIO_ID}`;
const url = `${base}/subscriptions/${GRATIO_TOKEN}`;
const payload = { name: "analytics", url: "https://bi.example.ru/gratio.php" };
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
console.log(response.status, await response.json());
PHP
<?php
$base = getenv('GRATIO_URL') . '/waInstance' . getenv('GRATIO_ID');
$url = $base . '/subscriptions/' . getenv('GRATIO_TOKEN');
$payload = ['name' => 'analytics', 'url' => 'https://bi.example.ru/gratio.php'];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
]);
$response = json_decode(curl_exec($ch), true);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), PHP_EOL;
print_r($response);
Вы увидите:
{"id": "sub_9f2c41d0", "name": "analytics", "url": "https://bi.example.ru/gratio.php",
"createdAt": 1792000000, "pending": 0, "oldestPendingAt": null, "dead": 0,
"lastDeliveredAt": null, "lastError": null, "lastErrorAt": null, "system": false,
"created": true, "secret": "5d0c7a…e81a"}
Сохраните secret в переменной GRATIO_SUB_SECRET на сервере приёмника. Больше его не покажут: потеряли — создайте подписку с тем же name и своим secret, адрес и очередь останутся.
Шаг 3. Проверьте
Напишите на номер канала с телефона и посмотрите, что пришло:
sqlite3 events.db "select type, chat_id, at from events order by at desc limit 5"
Вы увидите:
incomingMessageReceived|8695836|1791823658
Основное приложение получило то же сообщение своим путём — проверьте его журнал.
Шаг 4. Следите за доставкой
Список подписок показывает, сколько уведомлений ждут доставки и почему последняя не удалась:
curl "$GRATIO_URL/waInstance$GRATIO_ID/subscriptions/$GRATIO_TOKEN"
{"subscriptions": [{"id": "sub_9f2c41d0", "name": "analytics", "url": "https://bi.example.ru/gratio.php",
"createdAt": 1792000000, "pending": 0, "oldestPendingAt": null, "dead": 0,
"lastDeliveredAt": 1792000100, "lastError": null, "lastErrorAt": null, "system": false}]}
| Признак | Что значит | Что делать |
|---|---|---|
pending растёт, lastError — HTTP 500, тайм-аут |
приёмник недоступен; Gratio повторяет 1, 5, 15, 60, 300 с, затем каждые 5 минут — сутки | поднять приёмник, уведомления дойдут сами |
dead больше нуля, lastError — HTTP 401 или другой 4xx |
приёмник пять раз подряд отказал — уведомление отложено, очередь идёт дальше | проверить секрет и код приёмника |
system: true |
служебная подписка Gratio | не трогать: менять её ключом канала нельзя |
Шаг 5. Смените секрет или снимите подписку
Новый секрет — тот же POST с тем же name и своим secret (16–128 символов). Снять — DELETE с id подписки в конце пути:
curl -X DELETE "$GRATIO_URL/waInstance$GRATIO_ID/subscriptions/$GRATIO_TOKEN/sub_9f2c41d0"
Вы увидите: {"removed": true}.
Что дальше
- Подпись на других языках и проверочный пример — Проверка подписи.
- Все поля подписки и ограничения — subscriptions.
- Что лежит в теле уведомлений — Формат уведомления.
