В этом шаге Gratio начнёт присылать уведомления на ваш сервер сам — подпиской. Каждую доставку Gratio подписывает: обработчик проверяет подпись и принимает только то, что прислал Gratio. Опрашивать очередь не нужно.
Что понадобится
- переменные
GRATIO_URL,GRATIO_ID,GRATIO_TOKENиз Шаг 2. Первый запрос; - Python 3.10+ (
pip install fastapi uvicorn) или Node.js 18+ (npm install express); - публичный HTTPS-адрес, который ведёт на ваш обработчик. На своём компьютере подойдёт любой сервис туннелей, который выдаёт публичный HTTPS-адрес для локального порта.
Как работает подписка
Gratio Ваш сервер
│ POST /gratio/events │
│ X-Gratio-Signature: t=…,v1=… │
│ {уведомление} ────────────────────────────►│ проверить подпись
│ │ ответить сразу
│◄──────────────────────────────── 200 OK ───│
│ │ обработать в фоне
│ следующее уведомление ────────────────────►│
│ │
│ нет ответа или 5xx: │
│ повтор через 1, 5, 15, 60, 300 с … │
1. Придумайте секрет
Секрет подписи — случайная строка от 16 до 128 символов. Обработчик возьмёт его из переменной GRATIO_SUB_SECRET, а Gratio — из подписки.
macOS, Linux, Git Bash:
export GRATIO_SUB_SECRET=$(openssl rand -hex 32)
echo $GRATIO_SUB_SECRET
Windows PowerShell:
$env:GRATIO_SUB_SECRET = [guid]::NewGuid().ToString("N") + [guid]::NewGuid().ToString("N")
$env:GRATIO_SUB_SECRET
2. Запустите обработчик
Обработчик читает тело байтами, проверяет подпись, сразу отвечает 200 и обрабатывает уведомление после ответа. Как устроена подпись — в статье Проверка подписи.
Python
Файл events.py, запуск — uvicorn events:app --port 8080:
import hashlib
import hmac
import json
import os
import time
from fastapi import BackgroundTasks, FastAPI, HTTPException, Request
app = FastAPI()
SECRET = os.environ["GRATIO_SUB_SECRET"].encode()
def signed(body: bytes, header: str) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
t = parts.get("t", "")
if not t.isdigit() or abs(time.time() - int(t)) > 300:
return False
want = hmac.new(SECRET, f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, parts.get("v1", ""))
def process(event: dict) -> None:
print(event["typeWebhook"], event.get("senderData", {}).get("chatId"), event.get("messageData"))
@app.post("/gratio/events")
async def events(request: Request, tasks: BackgroundTasks) -> dict:
body = await request.body() # сырые байты — до разбора JSON
if not signed(body, request.headers.get("x-gratio-signature", "")):
raise HTTPException(status_code=401)
tasks.add_task(process, json.loads(body))
return {"ok": True}
Node.js
Файл events.mjs, запуск — node events.mjs:
import crypto from "node:crypto";
import express from "express";
const SECRET = process.env.GRATIO_SUB_SECRET;
function signed(body, header = "") {
const parts = {};
for (const piece of header.split(",")) {
const i = piece.indexOf("=");
if (i > 0) parts[piece.slice(0, i).trim()] = piece.slice(i + 1).trim();
}
const t = parts.t || "";
if (!/^\d+$/.test(t) || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const want = Buffer.from(crypto.createHmac("sha256", SECRET).update(`${t}.`).update(body).digest("hex"));
const got = Buffer.from(parts.v1 || "");
return want.length === got.length && crypto.timingSafeEqual(want, got);
}
const app = express();
// express.raw — тело остаётся байтами: подпись считают по ним
app.post("/gratio/events", express.raw({ type: "application/json", limit: "1mb" }), (req, res) => {
if (!Buffer.isBuffer(req.body) || !signed(req.body, req.get("x-gratio-signature"))) {
return res.sendStatus(401);
}
res.sendStatus(200);
const event = JSON.parse(req.body.toString("utf8"));
console.log(event.typeWebhook, event.senderData?.chatId, event.messageData);
});
app.listen(8080);
Вы увидите: сервер слушает порт 8080.
3. Откройте публичный адрес
Направьте туннель на порт 8080 или разместите обработчик на сервере с HTTPS. Адрес обработчика — например, https://example.ru/gratio/events.
Адрес должен быть публичным: внутренние и локальные адреса Gratio не принимает (400 bad_url).
4. Подпишите адрес на канал
Метод subscriptions: имя подписки, адрес и секрет. Замените адрес на свой, а секрет — на значение GRATIO_SUB_SECRET:
curl
curl -X POST "$GRATIO_URL/v1/instances/$GRATIO_ID/subscriptions" \
-H "Authorization: Bearer $GRATIO_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "app",
"url": "https://example.ru/gratio/events",
"secret": "5d0c3f1a9b7e4c2d8a6f0e1b3c5d7e9f"
}'
Python
import os
import requests
BASE = f"{os.environ['GRATIO_URL']}/v1/instances/{os.environ['GRATIO_ID']}"
HEADERS = {"Authorization": f"Bearer {os.environ['GRATIO_TOKEN']}"}
url = f"{BASE}/subscriptions"
payload = {
"name": "app",
"url": "https://example.ru/gratio/events",
"secret": "5d0c3f1a9b7e4c2d8a6f0e1b3c5d7e9f",
}
response = requests.post(url, json=payload, headers=HEADERS, timeout=60)
print(response.status_code, response.json())
Node.js
const { GRATIO_URL, GRATIO_ID, GRATIO_TOKEN } = process.env;
const base = `${GRATIO_URL}/v1/instances/${GRATIO_ID}`;
const auth = { Authorization: `Bearer ${GRATIO_TOKEN}` };
const url = `${base}/subscriptions`;
const payload = {
name: "app",
url: "https://example.ru/gratio/events",
secret: "5d0c3f1a9b7e4c2d8a6f0e1b3c5d7e9f",
};
const response = await fetch(url, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
console.log(response.status, await response.json());
PHP
<?php
$base = getenv('GRATIO_URL') . '/v1/instances/' . getenv('GRATIO_ID');
$auth = 'Authorization: Bearer ' . getenv('GRATIO_TOKEN');
$url = $base . '/subscriptions';
$payload = [
'name' => 'app',
'url' => 'https://example.ru/gratio/events',
'secret' => '5d0c3f1a9b7e4c2d8a6f0e1b3c5d7e9f',
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [$auth, '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:
{
"id": "sub_9f2c41d07be35a1c86e4f0b2",
"name": "app",
"url": "https://example.ru/gratio/events",
"createdAt": 1791823700,
"pending": 0,
"oldestPendingAt": null,
"dead": 0,
"lastDeliveredAt": null,
"lastError": null,
"lastErrorAt": null,
"system": false,
"created": true
}
5. Проверьте
Напишите на подключённый номер с телефона.
Вы увидите в консоли обработчика:
incomingMessageReceived 8695836 {'typeMessage': 'textMessage', 'textMessageData': {'textMessage': 'Здравствуйте!'}}
Как идёт доставка, показывает список подписок — pending, dead, lastDeliveredAt и lastError:
curl "$GRATIO_URL/v1/instances/$GRATIO_ID/subscriptions" \
-H "Authorization: Bearer $GRATIO_TOKEN"
Если не получилось
| Что видно | Причина | Что сделать |
|---|---|---|
в консоли ничего, в списке растёт pending и есть lastError |
адрес не открывается или обработчик падает | проверьте туннель и адрес; lastError назовёт причину |
обработчик отвечает 401 |
секрет в подписке и в GRATIO_SUB_SECRET разный |
повторите шаг 4 с секретом из переменной. Уведомление, отвергнутое 5 раз, уходит в отравленные — dead |
401 и секрет верный |
часы сервера отстают или спешат больше чем на 5 минут | включите синхронизацию времени |
400 bad_url |
адрес ведёт во внутреннюю сеть | дайте публичный адрес |
Правила для обработчика
- Отвечайте сразу, обработку делайте в фоне: Gratio ждёт ответа 30 секунд, а следующее уведомление шлёт после ответа на текущее.
- Проверяйте подпись по сырому телу — до разбора JSON.
- Отсекайте повторы по заголовку
X-Gratio-Deliveryили поeventId: если ответ потерялся, уведомление придёт снова. - Временный сбой — отвечайте
5xx: Gratio повторит. Ответ4xx— отказ: после 5 отказов уведомление уходит в отравленные (deadв списке подписок), очередь идёт дальше. - Незнакомое уведомление — тоже
200.
Подписка — основной способ получать уведомления на сервер: их у канала до пяти, у каждой своя очередь доставки, так что боту, CRM и аналитике хватит своей подписки на каждого. Для прототипа есть вариант проще — Вебхук: один адрес с токеном вместо подписи. Подробно о доставке — в статье subscriptions.
Что дальше
Канал подключён, сообщения уходят и приходят. Выберите сценарий:
Чат-бот на PythonБот, который отвечает клиентам в MAX, Telegram или WhatsApp по меню, присылает файл и зовёт оператора, — на очереди уведомлений, без публичного сервера. Переписка в своей CRMВстроить чаты MAX, Telegram и WhatsApp в свою CRM — входящие в карточку, ответ оператора, статусы, сообщения с телефона, правки, история и разрыв истории. Напоминания и рассылки без блокировокСкрипт напоминаний клиентам — выбор канала по номеру, рабочие часы, темп, правила WhatsApp, отложенные повторы и защита от дублей.Или изучите основы: Как проходят сообщения и уведомления, Темп и лимиты, Ответы и ошибки.
