Интеграции и API
Вебхуки, лента событий по ключу и текст для нейросети.
Всё, что происходит в проекте, складывается в одну ленту событий. Получатель либо подписывается на неё вебхуком, либо читает сам по ключу. Обычно нужно и то и другое: вебхук даёт скорость, лента — гарантию, что ничего не потерялось, пока приёмник лежал.
#Что настраивается в интерфейсе
Страница «Интеграции» в боковом меню:
- Приёмник — адрес, куда слать события, и маска типов («», «monitoring.», список через запятую). Секрет генерируется при создании и виден в списке.
- Ключ API — для чтения ленты. Показывается один раз: в базе лежит только его хеш. Ключ можно отозвать (запись останется в списке) или удалить совсем.
- Журнал доставок — что ушло, с каким ответом и сколько попыток. Кнопка «Повторить» доступна для окончательно не доставленных.
#Вебхук
POST с телом JSON и заголовками:
| Заголовок | Значение |
|---|---|
X-Telemaks-Event |
тип события, например monitoring.hit.created |
X-Telemaks-Delivery |
идентификатор доставки, ключ идемпотентности |
X-Telemaks-Timestamp |
unix-время отправки |
X-Telemaks-Signature |
t=<метка>,v1=<HMAC-SHA256> |
Подпись считается от строки <метка>.<тело> на секрете приёмника:
import hashlib, hmac
def check(secret: str, headers, body: bytes) -> bool:
stamp = headers["X-Telemaks-Timestamp"]
want = hmac.new(secret.encode(), stamp.encode() + b"." + body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(f"t={stamp},v1={want}",
headers["X-Telemaks-Signature"])
Метка входит в подпись, чтобы перехваченный запрос нельзя было повторить позже: отвергайте метки старше нескольких минут.
Успехом считается любой ответ 2xx. Иначе повтор через минуту, пять минут, пятнадцать, час и шесть часов; после этого доставка помечается мёртвой и ждёт человека. Отвечайте быстро — таймаут 10 секунд; тяжёлую работу делайте после ответа.
Одно и то же событие может прийти дважды (сеть оборвалась после вашего ответа) —
поэтому храните delivery_id и не обрабатывайте повторы.
#Лента
Адрес ТелеМакса — https://tmaks.ru, префикс API — /api/v1. Если принимающая
сторона просит «базовый адрес», это https://tmaks.ru; если полный адрес ленты, то
https://tmaks.ru/api/v1/integrations/events.
GET https://tmaks.ru/api/v1/integrations/events?since=<курсор>&type=monitoring.*&limit=100
X-API-Key: tmk_…
Ответ — голый массив событий, без объекта-обёртки. Пустая лента это [], а не
{"events": []}:
[
{
"id": "94b198d6-6a60-40b5-aecc-8208d61e758f",
"type": "monitoring.hit.created",
"created_at": "2026-09-15T19:56:49.172601Z",
"cursor": "2026-09-15T19:56:49.172601Z/94b198d6-6a60-40b5-aecc-8208d61e758f",
"data": { }
}
]
Обратите внимание: элемент ленты и тело вебхука устроены по-разному. У вебхука есть
конверт доставки (event, delivery_id, event_id, occurred_at, project_id), у
элемента ленты — id, type, created_at, cursor. Общее у них — поле data: его
содержимое совпадает байт в байт, так что разбирать полезную нагрузку можно одной
функцией.
Параметры: since — курсор, type — фильтр по типу (поддерживает monitoring.*),
rule — только события одной фразы, limit — от 1 до 500, по умолчанию 100.
#Одна кампания, а не весь проект
Ключ выдаётся на проект, а фраз в проекте обычно несколько: рядом с акцией живут постоянные темы. В общей ленте они перемешаны, и по одному тексту поста не всегда понятно, чем он пойман. Поэтому:
GET /api/v1/integrations/rules
X-API-Key: tmk_…
[{"id": "0ae9a967-…", "query": "\"пирог на четвертое\"", "enabled": true,
"hits_count": 15, "created_at": "2026-09-22T14:58:11Z"}]
и дальше …/events?rule=0ae9a967-…. Вместо id можно передать сам текст фразы —
сравнение без учёта регистра. Фильтр смотрит в событие, а не в список фраз, поэтому
работает и для удалённой фразы: события живут в ленте свои тридцать дней и после
её удаления, и без фильтра выглядят как чужие.
Порядок — по возрастанию времени. Сохраняйте cursor последнего обработанного события
и передавайте его в since: получите только то, что появилось после. Без since
отдаётся начало ленты. Признака «есть ли ещё» в ответе нет и не нужно: если вернулось
ровно limit записей, запрашивайте следующую страницу с курсором последней, и так пока
массив не станет короче limit.
Курсор непрозрачен — передавайте его обратно как есть, не разбирая на части. Метка
времени в нём заканчивается на Z именно для того, чтобы курсор можно было подставить
в адрес без кодирования. Вариант со смещением +00:00 тоже принимается, в том числе
если плюс превратился в пробел при наивной склейке адреса.
Лента хранится 30 дней.
Уведомления в Telegram работают независимо: подключение приёмника их не отключает и не заменяет — одно и то же попадание уходит и в телеграм, и в интеграцию.
#Конверт события
{
"event": "monitoring.hit.created",
"delivery_id": "e3b0c442-…",
"event_id": "8f14e45f-…",
"occurred_at": "2026-09-15T08:14:22.481Z",
"project_id": "…",
"cursor": "2026-09-15T08:14:22.481Z/8f14e45f-…",
"data": { }
}
В data для monitoring.hit.created:
{
"hit": {"id": "…"},
"rule": {"id": "…", "query": "#МыВместе2026"},
"source": {"platform": "max", "title": "Молодёжь Онлайн",
"username": "molodezh", "url": "https://max.ru/molodezh"},
"message": {"id": "…", "published_at": "2026-09-15T08:13:40Z",
"link": "https://max.ru/molodezh/1841",
"text": "…", "views": 12, "forwards": 34, "replies": 8,
"cluster_id": "…",
"media": [{"type": "photo", "url": "https://…jpg",
"thumbnail": null, "width": 1440, "height": 1814}]}
}
platform — tg, max, vk или web.
Полезная нагрузка вложенная, и это не случайность: rule отвечает на вопрос «чем
поймано», source — «где вышло», message — «что именно». Разбирайте её как
data.message.text, а не как плоский объект; при наивном разборе все события
выглядят одинаково пустыми.
#Вложения
message.media — до четырёх вложений поста, в порядке публикации:
| Поле | Что это |
|---|---|
type |
photo или video |
url |
сам файл, если он у нас есть; иначе null |
thumbnail |
картинка-превью; у видео есть только она |
width, height, duration |
когда площадка их сообщает |
Адреса абсолютные и готовы к вставке в <img>. Откуда они ведут, зависит от
площадки: у ВКонтакте это их CDN, у MAX — обложка видео на okcdn, у телеграма —
кадр с нашего сервера (tmaks.ru/media/…), у сайтов — картинка самого издания.
Файла видео нет ни у одной площадки: его не отдаёт никто, поэтому у видео
заполнено только thumbnail.
Чужие CDN живут своей жизнью и однажды ссылку закроют. Если вложение нужно надолго — в отчёте, в архиве кампании, — скачивайте его себе при получении события, а не ходите по ссылке год спустя.
Массив пуст, когда вложений у поста нет или площадка их не отдала.
#Отклик
views, forwards, replies — просмотры, пересылки и комментарии. Отдаются те,
которые площадка сообщает: пересылки есть у телеграма и ВКонтакте, комментарии —
только у телеграма, у MAX и у сайтов ни того ни другого нет. Там, где величины
нет, стоит null, а не 0: ноль означал бы «измерили и никто не переслал».
Цифры растут после публикации, поэтому в событии-находке они почти всегда
маленькие. Для отчёта берите событие monitoring.hit.metrics — оно приходит
с замерами через час и через сутки.
Историю фразы лента отдаёт тоже. Добавленная фраза сразу подтягивает прошлое —
на странице это видно как «найдено 15» в первую же минуту. Эти находки уходят в ленту
теми же событиями monitoring.hit.created, отдельной пометки у них нет: для отчёта по
кампании пост недельной давности ничем не хуже сегодняшнего. Разница только в
created_at события и message.published_at. Вглубь отдаётся до 2000 последних
находок на фразу — этого хватает на кампанию целиком; страница мониторинга при этом
знает всю историю и может показать больше.
cluster_id — справочное поле, и только. Каждый найденный пост приходит отдельным
событием: мы ничего не схлопываем и не считаем за одно, даже когда тексты совпадают.
Для инфокампании, где сообщества публикуют один и тот же текст с хештегом, это как раз
нужное поведение — принимающая сторона считает все посты. Поле можно просто игнорировать;
оно пригодится, если когда-нибудь понадобится обратное — отличить оригинал от перепечатки.
Для monitoring.hit.metrics добавляется:
{"metrics": {"stage": "d1", "views": 4120, "views_total": 4380}}
stage — h1 (замер через час после публикации) или d1 (через сутки). Событие
приходит по тому же hit.id, что и находка: просмотры в момент обнаружения почти
всегда околонулевые, для отчёта по кампании нужны именно эти замеры. Метрики
досылаются только проектам, у которых есть приёмник или ключ.
#Типы событий
| Тип | Когда |
|---|---|
ping |
кнопка «Проверить» в интерфейсе |
monitoring.hit.created |
мониторинг нашёл упоминание — новое или в истории добавленной фразы |
monitoring.hit.metrics |
появились замеры просмотров по найденному посту |
Новый тип добавляется в EVENT_TYPES (project/services/integrations/events.py)
и одним вызовом emit() в нужном месте — получатели и доставка уже есть.
#Про хештеги
Поиск идёт по словарю русского языка, и решётка при разборе отбрасывается: правило
#МыВместе2026 поймает и пост, где написано МыВместе2026 без решётки. Если для
кампании нужен строгий подсчёт именно хештегов, фильтруйте по тексту на своей
стороне — он приходит в message.text.