Перейти к основному содержимому

HTTP API

Используйте HTTP API для отправки событий напрямую из Telegram Mini App или с вашего сервера. Endpoint-а два, и выбор между ними — это выбор способа аутентификации:

  • Telegram WebApp / Mini App — из кода Mini App; пользователь опознаётся по подписанному initData.
  • Сервер → Metriox — с вашего бэкенда; аутентификация токеном бота в заголовке X-API-Key. Этим путём пользуются наши серверные SDK, и он же нужен, чтобы прислать в Metriox событие, которого Telegram не видел, — например платёж, проведённый через внешний эквайринг.

Telegram WebApp / Mini App

Основной endpoint для приложений на базе Telegram Mini App. Аутентификация происходит через initData из Telegram WebApp SDK — Metriox верифицирует подпись с помощью алгоритма Ed25519.

Endpoint

POST https://ingest.metriox.com/telegram/webapp

Структура запроса

{
"botId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"auth": {
"initData": "<window.Telegram.WebApp.initData>"
},
"events": [
{
"eventId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"eventName": "button_click",
"eventDate": "2026-04-26T10:00:00Z",
"eventType": "interaction",
"propsString": {
"button_name": "checkout",
"plan": "premium"
},
"propsLong": {
"price": 299
},
"propsBool": {
"is_trial": false
}
}
]
}

Поля запроса

ПолеТипОписание
botIdUUIDID бота из настроек Metriox
auth.initDatastringСтрока initData из window.Telegram.WebApp.initData
eventsarrayСписок событий (максимум 1000 за запрос)

projectId передавать не нужно — этого поля у endpoint нет. Проект определяется по botId на сервере, а присланный клиентом идентификатор проекта сознательно не используется: иначе им можно было бы писать события в чужой проект и на чужой счёт.

Поля события

ПолеОбязательноеТипОписание
eventIdUUIDУникальный ID события (для дедупликации)
eventNamestringНазвание события
eventDateISO 8601Дата и время события
eventTypestringКатегория события — см. список ниже
textstringТекстовое содержимое (до 4096 символов, дальше обрезается)
propsString{key: string}Строковые свойства события
propsLong{key: number}Целочисленные свойства
propsBool{key: boolean}Булевы свойства

Полей eventOrigin и platformUserId у этого endpoint нет, и передавать их не требуется. Origin проставляет сервер, а пользователь берётся из подписанного initData — это и делает идентификацию в Mini App достоверной.

Дробных чисел здесь нет

У WebApp-endpoint только три ведра свойств: строки, целые числа и булевы. Поля propsFloat не существует — если вы его пришлёте, оно будет молча проигнорировано. Дробное значение либо переведите в целое в понятной единице (rating_x100: 450), либо отправьте строкой.

Категории eventType

message · interaction · payment · membership · business · poll · reaction · boost · platform

Значение вне этого списка не отклоняет событие: оно записывается как platform, а в ответ добавляется предупреждение event_type_unknown. Если подходящей категории нет, отправляйте platform сразу.

Разделение props по типам

В отличие от flat props: {}, Metriox использует раздельные поля для каждого типа данных. Это позволяет использовать числовые операторы сравнения (>, <) в фильтрах.

SDK делает это автоматически.

Ограничения свойств

ОграничениеЗначение
Имя свойства^[A-Za-z_][A-Za-z0-9_.]{0,127}$ — до 128 символов
Длина строкового значения4096 символов, дальше обрезается с предупреждением
Количество свойств на событие1024, лишние отбрасываются
События в одном запросе1000

Имена, начинающиеся с $, зарезервированы за платформой: такое свойство отбрасывается с предупреждением prop_key_reserved, а само событие принимается.

Ответ

202 Accepted:

{
"accepted": 2,
"rejected": 1,
"not_consumed_duplicated": 0,
"stored_not_billed": 0,
"diagnostics_truncated": false,
"diagnostics": [
{
"event_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"code": "event_missing_identity",
"severity": "error",
"path": "event_name",
"message": "event_name is required."
}
]
}
ПолеОписание
acceptedСколько событий принято (у принятого события всё ещё могут быть предупреждения)
rejectedСколько отклонено — у каждого есть хотя бы одна диагностика с severity: "error"
not_consumed_duplicatedСколько событий отброшено как повтор eventId внутри запроса
stored_not_billedСколько событий записано, но не списано с квоты, потому что то же самое действие Telegram уже прислал другой источник. Это не отказ: события сохранены и видны в отчётах
diagnosticsСписок проблем: severity: "warning" — починено, событие записано; "error" — событие отклонено
diagnostics_truncatedtrue, если диагностик было больше 100 и список обрезан

Диагностику стоит читать: она называет конкретное свойство и причину, а не просто сообщает, что с запросом что-то не так.

Если отклонены все события батча, ответ будет 400 с тем же телом. Это сознательно: повторять такой запрос бессмысленно, он не пройдёт никогда.

Пример на JavaScript

async function trackEvent(eventName, props = {}, eventType = "platform") {
const initData = window.Telegram.WebApp.initData;

const propsString = {};
const propsLong = {};
const propsBool = {};

for (const [key, value] of Object.entries(props)) {
if (typeof value === "string") propsString[key] = value;
else if (typeof value === "boolean") propsBool[key] = value;
else if (Number.isInteger(value)) propsLong[key] = value;
// дробные числа этот endpoint не принимает — отправляем строкой
else if (typeof value === "number") propsString[key] = String(value);
}

const res = await fetch("https://ingest.metriox.com/telegram/webapp", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
botId: "YOUR_BOT_ID",
auth: { initData },
events: [
{
eventId: crypto.randomUUID(),
eventName,
eventDate: new Date().toISOString(),
eventType,
propsString,
propsLong,
propsBool,
},
],
}),
});

// повторять имеет смысл только 5xx (а также 408 и 429) — остальные 4xx не изменятся
if (!res.ok && res.status >= 500) scheduleRetry();
}

// Использование
trackEvent("button_click", { button_name: "checkout", price: 299, is_trial: false }, "interaction");
SDK

Для удобства используйте JavaScript SDK — он разделяет свойства по типам, управляет очередью и повторами и сам собирает контекст Mini App.

Время жизни initData

initData принимается, пока его auth_date не старше 15 минут. Строку можно использовать повторно: все батчи одной сессии уходят с одним и тем же initData, и это нормально — Telegram выдаёт её один раз при запуске и не обновляет. От повторной доставки защищает eventId, а не одноразовость initData.

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


Сервер → Metriox

Второй endpoint принимает события с вашего сервера: из обработчика вебхука Telegram, из фонового задания, из бэкенда, который проводит платежи. Аутентификация — токен бота в заголовке, а не initData, поэтому вызывать его можно откуда угодно, где есть HTTP-клиент.

Это тот же путь, которым пользуются серверные SDK — C# и Python.

Только с сервера

Токен даёт право писать события в ваш проект и расходовать вашу квоту. Ему нечего делать в браузере, в мобильном приложении или в Mini App — спрятать его там негде. Для Mini App есть WebApp-endpoint выше: он опознаёт пользователя по подписанному initData, а не по общему секрету.

Endpoint

POST https://ingest.metriox.com/tg

POST /telegram — тот же обработчик под вторым адресом. Новый код пишите на /tg.

Аутентификация

ЗаголовокЗначение
X-API-KeyТокен бота
Content-Typeapplication/json

Токен определяет и бота, и проект: botId и projectId в теле передавать не нужно — таких полей у endpoint нет. Один токен пишет события ровно одному боту.

Где взять токен

Токены выдаются на бота, а не на проект:

  1. Откройте приложение Metriox и перейдите в Проекты → нужный проект
  2. В таблице ботов, в строке нужного бота, нажмите Токены
  3. В окне Управление токенами нажмите Создать токен
  4. Скопируйте значение сразу — второй раз оно не показывается

Там же токены отзываются. Запрос с отозванным или неизвестным токеном получает 401.

Структура запроса

{
"events": [
{
"eventId": "3f1b2c7e-0a44-4d1e-9c2b-8d6f5a1e7b90",
"platformUserId": "123456789",
"eventOrigin": "custom",
"eventType": "interaction",
"eventName": "plan_selected",
"eventDate": "2026-04-26T10:00:00Z",
"propsString": {
"plan": "premium"
},
"propsLong": {
"seats": 3
},
"propsFloat": {
"discount_rate": 0.15
},
"propsBool": {
"is_trial": false
}
}
],
"users": [
{
"telegramUserId": 123456789,
"username": "ivanov",
"firstName": "Иван",
"isPremium": false
}
]
}

Поля запроса

ПолеОбязательноеТипОписание
eventsarrayСписок событий (максимум 1000 за запрос, пустой список отклоняется)
usersarrayПрофили людей из этого батча — кто они, а не что они сделали
botobjectСнимок самого бота: telegramBotId, name, description, starsAmount
versionstringВерсия контракта запроса. Не передавайте её — незнакомое значение даёт 400

Поля события

ПолеОбязательноеТипОписание
eventIdUUIDУникальный ID события (для дедупликации)
platformUserIdstringTelegram-id пользователя, строкой. См. раздел ниже
eventOriginstring"platform" или "custom" — см. раздел ниже
eventTypestringКатегория события — тот же список, что у WebApp-endpoint
eventNamestringНазвание события
eventDateISO 8601Дата и время события
textstringТекстовое содержимое сообщения (до 4096 символов)
propsString{key: string}Строковые свойства события
propsLong{key: number}Целочисленные свойства
propsFloat{key: number}Дробные свойства
propsBool{key: boolean}Булевы свойства

Поля с ✅ обязательны на уровне разбора: запрос, где у события нет хотя бы одного из них, принят не будет.

Здесь propsFloat есть

В отличие от WebApp-endpoint, у серверного пути четыре ведра свойств, а не три. Дробное число можно отправлять как есть.

Профили в users[] необязательны, но полезны: событие несёт id человека, а имя, @username, язык и признак Premium несёт только профиль. Без них пользователи в интерфейсе выглядят как голые числовые id.

platformUserId обязателен

Это числовой Telegram-id пользователя, переданный строкой: "123456789".

Событие без него отклоняется. В ответе приходит диагностика с severity: "error", кодом event_missing_identity и путём platform_user_id, событие попадает в счётчик rejected и не записывается. Если его нет ни у одного события в батче, весь запрос получает 400.

Отказ здесь — лучший из возможных исходов. Событие, не привязанное к человеку, нельзя ни соединить с остальной его историей, ни посчитать в уникальных пользователях, так что записать его «как-нибудь» значило бы тихо испортить все метрики по пользователям. Metriox вместо этого сразу говорит, чего не хватает.

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

eventOrigin: platform или custom

Это поле решает, как Metriox прочитает ваши свойства.

  • "platform" — событие описывает то же, что описал бы сам Telegram. Плоские ключи tg.<поле> из канонического реестра поднимаются в секцию $tg, и строка становится неотличимой по форме от той, что записал бы наш собственный сборщик.
  • "custom" — ваше бизнес-событие. Свойства сохраняются как есть, ничего никуда не поднимается.

Ошибиться здесь можно молча. Событие с "custom" примут и запишут, но tg.total_amount останется плоским пользовательским свойством tg.total_amount, а не станет $tg.total_amount. Готовые отчёты ищут канонические поля и такую строку не увидят: данные будут в системе, но не там, где их ждут, и молча.

Обратное тоже верно и тоже тихо: с "platform" ключ tg.<поле>, которого нет в реестре, остаётся плоским — поднимаются только известные поля.

Ограничения

ОграничениеЗначение
События в одном запросе1000
Частота запросов120 запросов в минуту на токен, с запасом на всплеск до 300
Свойства событияТе же правила, что у WebApp-endpoint — см. «Ограничения свойств»

Превышение частоты — 429, без очереди: запрос отклоняется сразу, а не ждёт. Лимит считается по токену, поэтому один шумный процесс не расходует лимит остальных ваших ботов.

Отдельного ограничения на размер тела запроса Metriox не задаёт: практический потолок задаёт лимит в 1000 событий, а дополнительные ограничения может накладывать прокси перед сервисом.

Коды ответов серверного endpoint

КодОписание
202Accepted — события приняты (в теле диагностика, как у WebApp-endpoint)
400Пустой батч, больше 1000 событий, незнакомая version либо отклонены все события
401Нет заголовка X-API-Key, токен неизвестен или отозван
402Payment Required — исчерпан лимит событий по тарифу
403Приём событий от SDK / API выключен в настройках бота — см. SDK
429Too Many Requests — превышена частота запросов
503Metriox временно не смог определить тариф проекта — повторите позже

Повторять имеет смысл 429 и 503. Остальные коды — вердикт: тот же запрос не пройдёт и со второй попытки.

Тело ответа то же, что у WebApp-endpoint: accepted, rejected, diagnostics и остальные счётчики. Диагностику стоит читать — она называет конкретное поле и причину.

Платёж из внешнего эквайринга

Большинство Telegram-ботов принимают деньги не через Telegram Payments, а через собственный эквайринг: платёж проходит на стороне вашего бэкенда, и Telegram о нём ничего не знает. Заводить для этого своё событие не нужно — отправьте то же самое successful_payment, которое Metriox записал бы для платежа внутри Telegram.

Из вебхука вашего платёжного провайдера, после подтверждения оплаты:

curl -X POST https://ingest.metriox.com/tg \
-H "X-API-Key: $METRIOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": [
{
"eventId": "9c1e4b7a-2d55-4a10-8f3b-6e0d9a2c4471",
"platformUserId": "123456789",
"eventOrigin": "platform",
"eventType": "payment",
"eventName": "successful_payment",
"eventDate": "2026-04-26T10:00:00Z",
"propsString": {
"tg.currency": "RUB",
"tg.invoice_payload": "order-10245",
"tg.provider_payment_charge_id": "acq-77f0c1e9"
},
"propsLong": {
"tg.total_amount": 49900
}
}
],
"users": [
{ "telegramUserId": 123456789, "username": "ivanov", "firstName": "Иван" }
]
}'

Что здесь важно:

  • eventOrigin"platform". Без него tg.currency и tg.total_amount останутся плоскими пользовательскими свойствами, и отчёты их не найдут.
  • tg.total_amount — целое число в propsLong. Так это поле объявлено в каноническом реестре и так его пишет наш собственный сборщик. Та же сумма строкой положила бы в то же поле значение другого типа.
  • Сумма — в наименьших единицах валюты. 499,00 ₽ — это 49900. В этих же единицах отчёт по выручке показывает результат.
  • platformUserId — Telegram-id покупателя, а не ваш внутренний id пользователя.
  • text отправлять не нужно — у платежа нет текста сообщения.
  • tg.invoice_payload — ваш номер заказа, tg.provider_payment_charge_id — id транзакции у эквайера. Оба поля канонические, оба необязательные, оба сильно облегчают сверку.

После этого отчёт Выручка (ОтчётыМонетизация) начинает показывать эти платежи без какой-либо настройки: он суммирует $tg.total_amount по событиям successful_payment, пришедшим по каналу Bot API, и группирует по $tg.currency — а это ровно то, что вы только что отправили.

У отчёта стоит пометка «Только Bot API SDK». Она про канал, а не про способ отправки: запрос на /tg приходит по тому же каналу, что и события из SDK, поэтому платежи из вашего эквайринга он учитывает.


Коды ответов

Коды WebApp-endpoint. Для серверного /tg смотрите таблицу в его собственном разделе выше.

КодОписание
202Accepted — события приняты (в теле диагностика)
400Bad Request — пустой батч, больше 1000 событий либо отклонены все события
401Unauthorized — пустой или невалидный initData, либо истёк его auth_date
402Payment Required — исчерпан лимит событий по тарифу
409Conflict — бот не привязан к проекту, не настроен в Telegram или ingest выключен
503Service Unavailable — временная проблема на стороне Metriox, повторите позже

Повторять имеет смысл 408, 429 и 5xx. Остальные 4xx — это вердикт: тот же запрос не пройдёт и со второй попытки.


Что списывается с квоты

Квота считается в событиях. Профили пользователей (users[], а также те, что Metriox достаёт из самих событий) не списываются, если приехали вместе с событиями — вы уже платите за действие, которое раскрыло личность, и одно обновление Telegram не должно стоить дважды.

Профиль, отправленный отдельно, стоит одно событие. Практический вывод: держите users[] в том же запросе, что и события, — так и дешевле, и меньше запросов.

Запрос вообще без событий отклоняется (400), поэтому загрузить свою базу пользователей одним users[] нельзя: профиль появляется вместе с первым действием человека.


Что дальше?

  • SDK — рекомендуемый способ интеграции
  • События — структура событий
  • Типы данных — какие типы свойств поддерживаются