ТелеМакс Документация
Работа с данными · Интеграции и API

Интеграции и API

Вебхуки, лента событий по ключу и текст для нейросети.

Скачать .md

Всё, что происходит в проекте, складывается в одну ленту событий. Получатель либо подписывается на неё вебхуком, либо читает сам по ключу. Обычно нужно и то и другое: вебхук даёт скорость, лента — гарантию, что ничего не потерялось, пока приёмник лежал.

#Что настраивается в интерфейсе

Страница «Интеграции» в боковом меню:

  • Приёмник — адрес, куда слать события, и маска типов («», «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.