Что получится: сервер на Node.js, который принимает уведомления канала вебхуком. Он проверяет секрет, отвечает Gratio сразу, отсекает повторы, раскладывает уведомления по видам — сообщения, статусы, состояние канала, звонки — и отвечает на входящие.
Gratio ──POST /gratio/hook──► проверить секрет ──► 200 сразу
│
▼
повтор? (eventId) ──да──► пропустить
│нет
▼
сообщение │ статус │ состояние │ звонок │ прочее
Что понадобится
- канал с ключом — Шаг 1. Подключите номер;
- переменные
GRATIO_URL,GRATIO_ID,GRATIO_TOKEN; - Node.js 18+;
- публичный HTTPS-адрес: сервер с доменом или туннель к локальному порту.
Шаг 1. Проект
mkdir gratio-hook && cd gratio-hook
npm init -y && npm pkg set type=module
npm install express
Придумайте секрет вебхука — длинную случайную строку:
export GRATIO_HOOK_TOKEN=$(openssl rand -hex 24)
Шаг 2. Сервер
Файл server.js. Обработчик отвечает 200 до обработки: Gratio ждёт ответа 30 секунд, а следующее уведомление уходит только после ответа на текущее.
import express from "express";
const { GRATIO_URL, GRATIO_ID, GRATIO_TOKEN, GRATIO_HOOK_TOKEN } = process.env;
const base = `${GRATIO_URL}/waInstance${GRATIO_ID}`;
const seen = new Map(); // eventId → время; в работе — Redis или таблица
const app = express();
app.use(express.json({ limit: "1mb" }));
app.post("/gratio/hook", (req, res) => {
if (req.get("authorization") !== `Bearer ${GRATIO_HOOK_TOKEN}`) return res.sendStatus(401);
res.sendStatus(200);
const body = req.body;
const key = body.eventId ?? `${body.typeWebhook}:${body.idMessage}:${body.status ?? ""}`;
if (seen.has(key)) return; // повтор после потерянного ответа
seen.set(key, Date.now());
route(body).catch((e) => console.error("обработка:", e));
});
app.listen(8080, () => console.log("Слушаю :8080"));
Шаг 3. Разбор уведомлений
Допишите в server.js маршрутизацию по typeWebhook и ответ на текст:
async function route(body) {
switch (body.typeWebhook) {
case "incomingMessageReceived":
return onIncoming(body);
case "outgoingMessageStatus":
return console.log(`статус ${body.idMessage}: ${body.status}`);
case "stateInstanceChanged":
return console.log(`канал: ${body.stateInstance}`);
case "incomingCall":
return console.log(`звонок от ${body.chatId}: ${body.status}`);
default:
return; // остальное пока не нужно
}
}
async function onIncoming(body) {
const { chatId, chatType, senderName } = body.senderData;
const data = body.messageData;
if (chatType !== "user" || data.typeMessage !== "textMessage") return;
const text = data.textMessageData.textMessage;
console.log(`${senderName}: ${text}`);
await send("sendMessage", {
chatId,
message: `Спасибо, ${senderName}! Ответим в течение 10 минут.`,
clientMessageId: `ack-${body.idMessage}`,
});
}
async function send(method, payload) {
const r = await fetch(`${base}/${method}/${GRATIO_TOKEN}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!r.ok) throw new Error(`${method}: ${r.status} ${await r.text()}`);
return r.json();
}
Запустите:
node server.js
Вы увидите: Слушаю :8080.
Шаг 4. Укажите адрес в канале
Откройте публичный адрес на порт 8080 и укажите его в настройках канала вместе с секретом. Замените адрес и секрет на свои:
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);
Вы увидите: {"saveSettings": true}.
Шаг 5. Проверьте
Напишите на номер канала с телефона.
Вы увидите в консоли:
Мария: Здравствуйте!
статус 117428956522361912: sent
статус 117428956522361912: read
…а на телефоне — ответ «Спасибо, Мария!».
Если обработчик падал
Уведомление, которое ваш сервер не принял 20 раз подряд или час, Gratio откладывает в отравленные, чтобы не держать очередь. После починки верните их:
curl "$GRATIO_URL/waInstance$GRATIO_ID/getDeadNotifications/$GRATIO_TOKEN"
curl -X POST "$GRATIO_URL/waInstance$GRATIO_ID/requeueDeadNotifications/$GRATIO_TOKEN" \
-H 'Content-Type: application/json' \
-d '{}'
Подробно — getDeadNotifications.
Полный код
import express from "express";
const { GRATIO_URL, GRATIO_ID, GRATIO_TOKEN, GRATIO_HOOK_TOKEN } = process.env;
const base = `${GRATIO_URL}/waInstance${GRATIO_ID}`;
const seen = new Map();
async function send(method, payload) {
const r = await fetch(`${base}/${method}/${GRATIO_TOKEN}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!r.ok) throw new Error(`${method}: ${r.status} ${await r.text()}`);
return r.json();
}
async function onIncoming(body) {
const { chatId, chatType, senderName } = body.senderData;
const data = body.messageData;
if (chatType !== "user" || data.typeMessage !== "textMessage") return;
console.log(`${senderName}: ${data.textMessageData.textMessage}`);
await send("sendMessage", {
chatId,
message: `Спасибо, ${senderName}! Ответим в течение 10 минут.`,
clientMessageId: `ack-${body.idMessage}`,
});
}
async function route(body) {
switch (body.typeWebhook) {
case "incomingMessageReceived":
return onIncoming(body);
case "outgoingMessageStatus":
return console.log(`статус ${body.idMessage}: ${body.status}`);
case "stateInstanceChanged":
return console.log(`канал: ${body.stateInstance}`);
case "incomingCall":
return console.log(`звонок от ${body.chatId}: ${body.status}`);
default:
return;
}
}
const app = express();
app.use(express.json({ limit: "1mb" }));
app.post("/gratio/hook", (req, res) => {
if (req.get("authorization") !== `Bearer ${GRATIO_HOOK_TOKEN}`) return res.sendStatus(401);
res.sendStatus(200);
const body = req.body;
const key = body.eventId ?? `${body.typeWebhook}:${body.idMessage}:${body.status ?? ""}`;
if (seen.has(key)) return;
seen.set(key, Date.now());
route(body).catch((e) => console.error("обработка:", e));
});
// чистим память о повторах старше суток
setInterval(() => {
const day = Date.now() - 86_400_000;
for (const [k, t] of seen) if (t < day) seen.delete(k);
}, 3_600_000);
app.listen(8080, () => console.log("Слушаю :8080"));
Что дальше
- Все виды уведомлений и их поля — Формат уведомления.
- Отдать ту же ленту ещё одной системе, не мешая этой, — Вторая система на той же ленте.
- Правила доставки, повторы и коды ответа — Вебхук.
