Уведомления на ваш сервер

Возможность открывает владелец сервиса. Пока она не открыта для вас, раздела «Уведомления на ваш сервер» в кабинете нет.

Для тех, у кого своя система: учёт заявок, 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: после разбора и обратной сборки порядок полей и пробелы могут измениться, и подпись не сойдётся.