Уведомления на ваш сервер
Возможность открывает владелец сервиса. Пока она не открыта для вас, раздела «Уведомления на ваш сервер» в кабинете нет.
Для тех, у кого своя система: учёт заявок, CRM, сайт. Когда у ваших маршрутов что-то происходит — копия доставлена или нет, маршрут поставили на паузу, бот потерял доступ к чату, — наш сервер сам отправляет запрос на ваш адрес. Спрашивать нас каждую минуту не нужно.
Текстов сообщений и данных людей в уведомлениях нет: только номера, названия чатов и маршрутов, род отказа.
Где завести
Кабинет → «Ключи доступа» → «Уведомления на ваш сервер» → «Новый адрес». Адрес — только https://; адреса во внутренней сети и перенаправления не принимаются. До 10 адресов.
После заведения кабинет один раз показывает секрет подписи — сохраните его на своём сервере. Потеряли — «Сменить секрет».
«Проверить» отправляет пробное уведомление вида test и сразу показывает, что ответил ваш сервер.
События
| Событие | Когда |
|---|---|
message.relayed |
копия доставлена в приёмник |
message.failed |
площадка не приняла копию |
route.paused |
маршрут поставили на паузу |
route.resumed |
маршрут снова включён |
chat.access_lost |
бота убрали из чата или лишили прав |
test |
пробное уведомление из кабинета |
Запрос
POST на ваш адрес, тело — JSON, заголовки:
| Заголовок | Что в нём |
|---|---|
X-Aibo-Event-Id |
номер события; у повтора — тот же. По нему отбрасывайте повторы |
X-Aibo-Event-Type |
вид события |
X-Aibo-Timestamp |
время отправки, секунды Unix |
X-Aibo-Signature |
v1= и HMAC-SHA256 в шестнадцатеричном виде |
Тело message.relayed:
{
"id": "0c1f4e2a-…",
"type": "message.relayed",
"occurred_at": "2026-10-18T10:12:03.000Z",
"attempt": 1,
"data": {
"route": { "id": 190, "name": "Гороскоп" },
"source": { "chat": { "platform": "telegram", "id": "-1001234567890", "title": "Гороскоп" }, "message_id": "1534" },
"destination": { "chat": { "platform": "max", "id": "-71234567", "title": "Гороскоп в MAX" }, "message_id": "mid.0000…" }
}
}
У message.failed в data ещё failure — род отказа: no_rights, chat_gone, too_long, rate_limited и другие. У route.* — route (с state), source и destination без номеров сообщений. У chat.access_lost — chat.
Ответ и повторы
Ответьте кодом 2xx в течение 10 секунд — уведомление доставлено. Любой другой ответ или молчание — повтор: через 15 секунд, затем с удвоением до часа, до 8 попыток. Если ваш сервер много раз подряд не принимает уведомления, мы отключаем адрес и пишем вам в личку бота. Исправьте сервер и нажмите «Возобновить».
Журнал последних доставок — на странице адреса: событие, время, код ответа, попытка. Недоставленное можно «Повторить».
Проверка подписи
Строка для подписи — значение X-Aibo-Timestamp, точка и тело запроса как пришло, байт в байт. Ключ — ваш секрет. Сверьте подпись и время: расхождение больше 5 минут — отвергайте, так перехваченный запрос нельзя повторить позже.
После «Сменить секрет» держите прежний секрет ещё сутки и принимайте подпись любым из двух: уведомления, ушедшие до смены, подписаны прежним.
JavaScript (Node.js):
import { createHmac, timingSafeEqual } from 'node:crypto';
function podlinnoe(telo, zagolovki, sekret) {
const vremya = Number(zagolovki['x-aibo-timestamp']);
if (Math.abs(Date.now() / 1000 - vremya) > 300) return false;
const zhdyom = Buffer.from('v1=' + createHmac('sha256', sekret).update(`${vremya}.${telo}`).digest('hex'));
const prishlo = Buffer.from(zagolovki['x-aibo-signature'] ?? '');
return zhdyom.length === prishlo.length && timingSafeEqual(zhdyom, prishlo);
}
Python:
import hmac, hashlib, time
def podlinnoe(telo: bytes, zagolovki, sekret: str) -> bool:
vremya = int(zagolovki["X-Aibo-Timestamp"])
if abs(time.time() - vremya) > 300:
return False
zhdyom = "v1=" + hmac.new(sekret.encode(), f"{vremya}.".encode() + telo, hashlib.sha256).hexdigest()
return hmac.compare_digest(zhdyom, zagolovki.get("X-Aibo-Signature", ""))
PHP:
function podlinnoe(string $telo, array $zagolovki, string $sekret): bool {
$vremya = (int) $zagolovki['X-Aibo-Timestamp'];
if (abs(time() - $vremya) > 300) return false;
$zhdyom = 'v1=' . hash_hmac('sha256', $vremya . '.' . $telo, $sekret);
return hash_equals($zhdyom, $zagolovki['X-Aibo-Signature'] ?? '');
}
Тело берите сырым, до разбора JSON: после разбора и обратной сборки порядок полей и пробелы могут измениться, и подпись не сойдётся.