Перейти к содержимому

Вебхук

Обновлено 17 октября 2026

Вебхук — адрес вашего сервера, на который Gratio сам присылает каждое уведомление запросом POST. Опрашивать очередь не нужно: уведомление приходит, как только событие записано.

Что получится: обработчик, который проверяет токен, сразу отвечает 200 и обрабатывает уведомление в фоне, без дублей.

Что понадобится

  • канал с ключом — Шаг 1. Подключите номер
  • Python 3.10+ / Node.js 18+ / PHP 8+
  • публичный HTTPS-адрес; для разработки хватит туннеля — шаг 3

Как это работает

Каждое уведомление — отдельный запрос на ваш адрес:

Gratio                                       Ваш сервер
  │ POST /gratio/hook                           │
  │ Authorization: Bearer s3cr3t-hook-token     │
  │ {"typeWebhook": "incomingMessageReceived"…} │
  │ ───────────────────────────────────────────►│ проверить токен
  │◄──────────────────────────────────── 200 OK │ ответить сразу
  │                                             │ обработать в фоне
  │ POST /gratio/hook  следующее уведомление ──►│
  • Уведомления идут по одному и по порядку: следующее — после того, как принято текущее.
  • Ответ 2xx — доставлено. Тело ответа Gratio не читает.
  • Gratio ждёт ответа до 30 секунд. Отвечайте сразу, долгую работу делайте после ответа.
  • Не 2xx, тайм-аут или сбой сети — повтор. Расписание — ниже, в разделе «Ответ и повторы».

Шаг 1. Напишите обработчик

Обработчик делает четыре вещи:

  1. Сверяет заголовок Authorization с вашим токеном — так он принимает уведомления только от Gratio.
  2. Отсекает повтор по ключу события: eventId, а у уведомлений без него — хеш тела. Повтор приходит тем же телом.
  3. Отвечает 200 сразу.
  4. Обрабатывает уведомление после ответа.

Токен — любая длинная случайная строка. В примерах он в переменной GRATIO_HOOK_TOKEN, значение — s3cr3t-hook-token.

Python (FastAPI)

Файл hook.py:

import hashlib
import hmac
import json
import os

from fastapi import BackgroundTasks, FastAPI, Header, HTTPException

app = FastAPI()
EXPECTED = f"Bearer {os.environ['GRATIO_HOOK_TOKEN']}"
seen: set[str] = set()  # в работе — таблица в базе или Redis


def event_key(body: dict) -> str:
    """eventId, а если его нет (статусы, состояние канала) — хеш тела: повтор приходит тем же телом."""
    if body.get("eventId"):
        return body["eventId"]
    return hashlib.sha256(json.dumps(body, sort_keys=True).encode()).hexdigest()


def process(body: dict) -> None:
    if body["typeWebhook"] == "incomingMessageReceived":
        sender = body["senderData"]
        print(sender["chatId"], sender["senderName"], body["messageData"]["typeMessage"])


@app.post("/gratio/hook")
async def gratio_hook(body: dict, tasks: BackgroundTasks, authorization: str = Header("")) -> dict:
    if not hmac.compare_digest(authorization, EXPECTED):
        raise HTTPException(status_code=401)
    key = event_key(body)
    if key not in seen:
        seen.add(key)
        tasks.add_task(process, body)  # выполнится после ответа
    return {"ok": True}

Запуск:

pip install fastapi uvicorn
GRATIO_HOOK_TOKEN=s3cr3t-hook-token uvicorn hook:app --port 8000

Node.js (Express)

Файл hook.mjs:

import crypto from "node:crypto";
import express from "express";

const expected = Buffer.from(`Bearer ${process.env.GRATIO_HOOK_TOKEN}`);
const seen = new Set(); // в работе — таблица в базе или Redis

function authorized(header = "") {
  const got = Buffer.from(header);
  return got.length === expected.length && crypto.timingSafeEqual(got, expected);
}

// eventId, а если его нет (статусы, состояние канала) — хеш тела: повтор приходит тем же телом
function eventKey(body) {
  return body.eventId || crypto.createHash("sha256").update(JSON.stringify(body)).digest("hex");
}

function processEvent(body) {
  if (body.typeWebhook === "incomingMessageReceived") {
    const { chatId, senderName } = body.senderData;
    console.log(chatId, senderName, body.messageData.typeMessage);
  }
}

const app = express();
app.use(express.json({ limit: "1mb" })); // письма бывают больше 100 КБ — предела по умолчанию

app.post("/gratio/hook", (req, res) => {
  if (!authorized(req.get("authorization"))) return res.sendStatus(401);
  const key = eventKey(req.body);
  res.json({ ok: true }); // ответить сразу
  if (seen.has(key)) return;
  seen.add(key);
  setImmediate(() => processEvent(req.body));
});

app.listen(8000);

Запуск:

npm install express
GRATIO_HOOK_TOKEN=s3cr3t-hook-token node hook.mjs

PHP

Файл hook.php на вашем веб-сервере:

<?php
$expected = 'Bearer ' . getenv('GRATIO_HOOK_TOKEN');
if (!hash_equals($expected, $_SERVER['HTTP_AUTHORIZATION'] ?? '')) {
    http_response_code(401);
    exit;
}

$raw = file_get_contents('php://input');
$body = json_decode($raw, true);
if (!is_array($body)) {
    http_response_code(400);
    exit;
}
// eventId, а если его нет (статусы, состояние канала) — хеш тела: повтор приходит тем же телом
$key = $body['eventId'] ?? hash('sha256', $raw);

header('Content-Type: application/json');
echo '{"ok":true}';
if (function_exists('fastcgi_finish_request')) {
    fastcgi_finish_request(); // ответ ушёл, дальше — обработка
}

if (!already_seen($key)) {
    process_event($body);
}

function already_seen(string $key): bool
{
    // в работе — уникальный ключ в таблице базы
    $mark = sys_get_temp_dir() . '/gratio-' . hash('sha256', $key);
    if (file_exists($mark)) {
        return true;
    }
    touch($mark);
    return false;
}

function process_event(array $body): void
{
    if ($body['typeWebhook'] === 'incomingMessageReceived') {
        error_log($body['senderData']['chatId'] . ' ' . $body['messageData']['typeMessage']);
    }
}

Apache с PHP-FPM иногда не передаёт PHP заголовок Authorization. Тогда добавьте в настройки сайта CGIPassAuth On.

Шаг 2. Проверьте обработчик локально

Пришлите обработчику уведомление сами — так, как его пришлёт Gratio:

curl -i -X POST http://localhost:8000/gratio/hook \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer s3cr3t-hook-token' \
  -d '{"typeWebhook": "incomingMessageReceived", "instanceData": {"idInstance": 7100000001, "wid": "79990000001@c.us", "typeInstance": "v3", "channel": "max"}, "timestamp": 1791823658, "idMessage": "117428955282341903", "eventId": "msg:8695836:117428955282341903", "senderData": {"chatId": "8695836", "chatName": "Мария", "chatType": "user", "sender": "8695836", "senderName": "Мария", "senderType": "user", "senderContactName": "", "senderPhoneNumber": 0}, "messageData": {"typeMessage": "textMessage", "textMessageData": {"textMessage": "Здравствуйте!"}}}'

Вы увидите: HTTP/1.1 200 OK, а в выводе обработчика — строку 8695836 Мария textMessage. Повторите запрос — ответ снова 200, а строки в выводе не прибавится: повтор отсечён. С другим токеном — 401.

Шаг 3. Откройте туннель для разработки

Gratio доставляет только на публичный адрес: localhost и адреса внутренней сети не подходят (400 bad_url). Для разработки откройте туннель — сервис, который выдаёт публичный HTTPS-адрес и пересылает запросы на ваш компьютер. Подойдёт любой, например ngrok или cloudpub.

Направьте туннель на порт 8000 и возьмите выданный адрес. Адрес вебхука — он плюс путь обработчика: https://<адрес туннеля>/gratio/hook.

Вы увидите: тот же curl из шага 2, отправленный на адрес туннеля, отвечает 200.

Шаг 4. Задайте адрес вебхука

Передайте адрес и токен в setSettings:

curl

curl -X POST "$GRATIO_URL/waInstance$GRATIO_ID/setSettings/$GRATIO_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{
    "webhookUrl": "https://example.ru/gratio/hook",
    "webhookUrlToken": "s3cr3t-hook-token"
  }'

Python

import os
import requests

BASE = f"{os.environ['GRATIO_URL']}/waInstance{os.environ['GRATIO_ID']}"
TOKEN = os.environ["GRATIO_TOKEN"]

url = f"{BASE}/setSettings/{TOKEN}"
payload = {
    "webhookUrl": "https://example.ru/gratio/hook",
    "webhookUrlToken": "s3cr3t-hook-token",
}
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}/setSettings/${GRATIO_TOKEN}`;
const payload = {
  webhookUrl: "https://example.ru/gratio/hook",
  webhookUrlToken: "s3cr3t-hook-token",
};
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 . '/setSettings/' . getenv('GRATIO_TOKEN');
$payload = [
    'webhookUrl' => 'https://example.ru/gratio/hook',
    'webhookUrlToken' => 's3cr3t-hook-token',
];

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

Ответ 200 OK:

{"saveSettings": true}

Настройка действует сразу. Уведомления, которые ждали в очереди, уйдут на вебхук по порядку. Адрес во внутренней сети — 400 bad_url; Gratio проверяет адрес и перед каждой доставкой.

Шаг 5. Проверьте доставку

Напишите на номер канала с другого телефона.

Вы увидите: в выводе обработчика — строку с chatId собеседника, его именем и textMessage. Очередь пуста:

curl "$GRATIO_URL/waInstance$GRATIO_ID/getWebhooksCount/$GRATIO_TOKEN"

Ответ {"count": 0}. Число растёт — Gratio не может доставить уведомление: проверьте ответы обработчика.

Что присылает Gratio

Запрос POST на webhookUrl, тело — уведомление целиком, как body в receiveNotification:

Заголовок Значение
Content-Type application/json
Authorization Bearer {webhookUrlToken} — если токен задан

Подписи у вебхука нет — сверяйте токен. Нужна подпись каждой доставки — используйте subscriptions.

Ответ и повторы

Ответ вашего сервера Что сделает Gratio
2xx за 30 секунд доставлено, следующее уведомление — сразу
3xx неудача: перенаправления Gratio не выполняет — укажите конечный адрес
4xx, 5xx неудача, повтор по расписанию
нет ответа за 30 секунд, сбой сети или сертификата неудача, повтор по расписанию
адрес стал вести во внутреннюю сеть неудача с причиной internal_host, повтор
неудачи час подряд или 20 попыток уведомление уходит в отравленные, очередь идёт дальше

Расписание повторов одного уведомления:

Попытка После первой
1 сразу
2 через 1 с
3 через 6 с
4 через 21 с
5 через 1 мин 21 с
6 через 6 мин 21 с
7 и дальше каждые 5 минут

При таких паузах раньше наступает час, а не 20 попыток: около 17-й неудачной попытки уведомление уходит в отравленные. Пока уведомление в повторах, следующие ждут за ним — порядок не нарушается.

Повторы и дубли

Gratio повторяет уведомление, если не получил 2xx, — даже когда вы его обработали, но ответ потерялся. Храните ключи обработанных уведомлений:

  • eventId — есть у сообщений, правок, удалений, реакций, звонков, событий групп и syncGap;
  • у outgoingAPIMessageReceived, outgoingMessageStatus и stateInstanceChanged его нет — берите хеш тела или пару idMessage + status.

Подробно о ключах — Формат уведомления.

Если сервер был недоступен

  • Уведомления ждут в очереди до 24 часов, затем истекают.
  • Уведомление в голове очереди после часа неудач уходит в отравленные, следующее начинает свой час. За каждый час простоя в отравленные уходит одно уведомление.
  • Сервер снова отвечает — посмотрите getDeadNotifications и верните их requeueDeadNotifications.
  • Истёкшие уведомления не вернуть, но сообщения остались в истории чата — getChatHistory.

Вернуться к очереди

Пустой webhookUrl выключает вебхук. Недоставленные уведомления остаются в очереди — их отдаст receiveNotification:

curl -X POST "$GRATIO_URL/waInstance$GRATIO_ID/setSettings/$GRATIO_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"webhookUrl": ""}'

Что дальше

Была ли статья полезна?