ТелеМакс Документация
Работа с данными · Интеграции и 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 работают независимо: подключение приёмника их не отключает и не заменяет — одно и то же попадание уходит и в телеграм, и в интеграцию.

#Цифры канала по ссылке

Отдельный способ, не связанный с мониторингом: даёте ссылку на канал, паблик, сайт или пост — получаете подписчиков, прирост, просмотры и вовлечённость из каталога ТелеМакса. Ключ тот же, что для ленты, лимиты запросов — по тарифу проекта.

Каналы из источников вашего проекта отдаются без ограничений. Цифры остальных каналов идут по квоте аккаунта: на Free — 2 000 разных каналов за всё время, на Pro и Max — 3 000 в месяц. Считается каждый канал отдельно, в том числе внутри списка; повторный запрос того же канала квоту не тратит. Сколько осталось, видно в заголовках ответа X-Channel-Quota-Limit и X-Channel-Quota-Left.

GET https://tmaks.ru/api/v1/integrations/channel-stats?url=https://t.me/rian_ru
X-API-Key: tmk_…

Понимает ссылки Telegram (t.me/ник, @ник, ссылка на пост), ВКонтакте (паблик, страница, пост wall-…), MAX (max.ru/ник), Одноклассников, Rutube (канал или ролик — берётся автор) и адрес сайта. Ссылка на пост ведёт к каналу, который его опубликовал.

{
  "url": "https://t.me/rian_ru",
  "found": true,
  "reason": null,
  "platform": "tg",
  "platform_name": "Telegram",
  "title": "РИА Новости",
  "handle": "rian_ru",
  "link": "https://t.me/rian_ru",
  "card": "https://tmaks.ru/c/tg/rian_ru",
  "subscribers": 3030098,
  "audience_kind": "subscribers",
  "visitors_month": null,
  "subscribers_at": "2026-10-17T18:46:55Z",
  "growth": {"day": -276, "week": -3715, "month": 18700},
  "views": {"avg": 200652, "median": 202002, "max_30d": 299214},
  "er": 0.83,
  "err": 6.62,
  "posts_30d": 1680,
  "last_post_at": "2026-10-17T16:04:19Z",
  "stats_at": null
}
Поле Что это
subscribers подписчики; у сайта — посетители в сутки (audience_kind: visitors_per_day), за месяц — visitors_month
growth прирост подписчиков за сутки, 7 и 30 дней; у сайта null
views средние и медианные просмотры поста за 30 дней и самый просматриваемый пост
er вовлечённость, %: реакции к просмотрам за 30 дней
err охват, %: средние просмотры поста к подписчикам
subscribers_at когда измерены подписчики
stats_at у каналов, которые ТелеМакс не читает постоянно, — когда сборщик последний раз считал просмотры; у читаемых null, их цифры свежие всегда
card карточка в каталоге; null, если канал есть в базе, но ещё не опубликован

Пустое значение — цифры нет, а не ноль: например, у канала без постов за месяц нет просмотров.

Ответы без цифр — тоже 200, с "found": false и причиной:

reason Что значит
not_in_catalog канала пока нет в каталоге. ТелеМакс сразу начинает его читать и добавит в каталог — повторите запрос через несколько часов, обычно хватает суток. В сутки так добавляется до 50 новых каналов на проект
unsupported площадку пока не читаем (YouTube, Дзен, Instagram и др., в platform — какая). Запрос записан: по таким запросам решаем, что подключать следующим
unrecognized ссылку не удалось разобрать
quota квота цифр каналов не из ваших источников исчерпана. Добавьте канал в источники проекта, перейдите на тариф больше или повторите запрос с over_quota=true

Списком — до 100 ссылок за запрос, ответ в том же порядке:

POST https://tmaks.ru/api/v1/integrations/channel-stats
X-API-Key: tmk_…
Content-Type: application/json

{"urls": ["https://t.me/rian_ru", "https://vk.com/ria", "lenta.ru"]}

Цифры берутся из каталога, а не с площадки в момент запроса: подписчики обновляются несколько раз в сутки, просмотры и ER — раз в сутки.

#Конверт события

{
  "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"},
  "match": {"kind": "word", "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, ok (Одноклассники) или rutube.

match — как пост пойман (с 29.10.2026):

kind Что значит query
word по словам самой фразы фраза
sem по смыслу: слов фразы в посте может не быть фраза

Полезная нагрузка вложенная, и это не случайность: rule отвечает на вопрос «чем поймано», source — «где вышло», message — «что именно». Разбирайте её как data.message.text, а не как плоский объект; при наивном разборе все события выглядят одинаково пустыми.

#Вложения

message.media — до четырёх вложений поста, в порядке публикации:

Поле Что это
type photo или video
url сам файл, если он у нас есть; иначе null
thumbnail картинка-превью; у видео есть только она
width, height, duration когда площадка их сообщает

Адреса абсолютные и готовы к вставке в <img>. Откуда они ведут, зависит от площадки: у ВКонтакте это их CDN, у MAX — обложка видео на okcdn, у телеграма — кадр с нашего сервера (tmaks.ru/media/…) или с CDN телеграма (cdn*.telesco.pe), у сайтов — картинка самого издания. У телеграма тип вложения не всегда известен: кадр ролика может прийти как photo. Файла видео нет ни у одной площадки: его не отдаёт никто, поэтому у видео заполнено только thumbnail.

Пустой media бывает у закрытого канала телеграма: картинку такого поста взять неоткуда, кроме самого телеграма.

Чужие 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 появились замеры просмотров по найденному посту
monitoring.story.rising сюжет с фразой проекта (галочка «Сигнал о росте») или из его регионов и тем начал расти — одно событие на сюжет

У monitoring.story.rising в data: story (id, title, summary, url, theme, region — главные; themes, regions — все темы и места сюжета), metrics (posts, posts_last_hour, channels, new_channels_last_hour, platforms, regions, federal_channels, first_published_at, score) и reasons — за что пришло: phrases, region, theme.

Новый тип добавляется в EVENT_TYPES (project/services/integrations/events.py) и одним вызовом emit() в нужном месте — получатели и доставка уже есть.

#Про хештеги

Поиск идёт по словарю русского языка, и решётка при разборе отбрасывается: правило #МыВместе2026 поймает и пост, где написано МыВместе2026 без решётки. Если для кампании нужен строгий подсчёт именно хештегов, фильтруйте по тексту на своей стороне — он приходит в message.text.

#Сверх квоты — за токены

Когда квота чужих каналов кончилась, добавьте к запросу ?over_quota=true — и для GET, и для POST /channel-stats. Тогда каждый следующий канал спишет 1 000 токенов из месячного лимита тарифа: того же, что тратят рерайт и AI-редактор. Повторный запрос того же канала в этом месяце не списывает ничего. Сколько списано за запрос, — в заголовке X-Tokens-Spent. Без параметра токены не тратятся никогда.