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
}
}
]
}
Поля запроса
| Поле | Тип | Описание |
|---|---|---|
botId | UUID | ID бота из настроек Metriox |
auth.initData | string | Строка initData из window.Telegram.WebApp.initData |
events | array | Список событий (максимум 1000 за запрос) |
projectId передавать не нужно — этого поля у endpoint нет. Проект определяется по botId на сервере, а присланный клиентом идентификатор проекта сознательно не используется: иначе им можно было бы писать события в чужой проект и на чужой счёт.
Поля события
| Поле | Обязательное | Тип | Описание |
|---|---|---|---|
eventId | ✅ | UUID | Уникальный ID события (для дедупликации) |
eventName | ✅ | string | Название события |
eventDate | ✅ | ISO 8601 | Дата и время события |
eventType | ✅ | string | Категория события — см. список ниже |
text | ❌ | string | Текстовое содержимое (до 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 сразу.
В отличие от 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_truncated | true, если диагностик было больше 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");
Для удобства используйте 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-Type | application/json |
Токен определяет и бота, и проект: botId и projectId в теле передавать не нужно — таких полей у endpoint нет. Один токен пишет события ровно одному боту.
Где взять токен
Токены выдаются на бота, а не на проект:
- Откройте приложение Metriox и перейдите в Проекты → нужный проект
- В таблице ботов, в строке нужного бота, нажмите Токены
- В окне Управление токенами нажмите Создать токен
- Скопируйте значение сразу — второй раз оно не показывается
Там же токены отзываются. Запрос с отозванным или неизвестным токеном получает 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
}
]
}
Поля запроса
| Поле | Обязательное | Тип | Описание |
|---|---|---|---|
events | ✅ | array | Список событий (максимум 1000 за запрос, пустой список отклоняется) |
users | ❌ | array | Профили людей из этого батча — кто они, а не что они сделали |
bot | ❌ | object | Снимок самого бота: telegramBotId, name, description, starsAmount |
version | ❌ | string | Версия контракта запроса. Не передавайте её — незнакомое значение даёт 400 |
Поля события
| Поле | Обязательное | Тип | Описание |
|---|---|---|---|
eventId | ✅ | UUID | Уникальный ID события (для дедупликации) |
platformUserId | ✅ | string | Telegram-id пользователя, строкой. См. раздел ниже |
eventOrigin | ✅ | string | "platform" или "custom" — см. раздел ниже |
eventType | ✅ | string | Категория события — тот же список, что у WebApp-endpoint |
eventName | ✅ | string | Название события |
eventDate | ✅ | ISO 8601 | Дата и время события |
text | ❌ | string | Текстовое содержимое сообщения (до 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
| Код | Описание |
|---|---|
| 202 | Accepted — события приняты (в теле диагностика, как у WebApp-endpoint) |
| 400 | Пустой батч, больше 1000 событий, незнакомая version либо отклонены все события |
| 401 | Нет заголовка X-API-Key, токен неизвестен или отозван |
| 402 | Payment Required — исчерпан лимит событий по тарифу |
| 403 | Приём событий от SDK / API выключен в настройках бота — см. SDK |
| 429 | Too Many Requests — превышена частота запросов |
| 503 | Metriox временно не смог определить тариф проекта — повторите позже |
Повторять имеет смысл 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 смотрите таблицу в его собственном разделе выше.
| Код | Описание |
|---|---|
| 202 | Accepted — события приняты (в теле диагностика) |
| 400 | Bad Request — пустой батч, больше 1000 событий либо отклонены все события |
| 401 | Unauthorized — пустой или невалидный initData, либо истёк его auth_date |
| 402 | Payment Required — исчерпан лимит событий по тарифу |
| 409 | Conflict — бот не привязан к проекту, не настроен в Telegram или ingest выключен |
| 503 | Service Unavailable — временная проблема на стороне Metriox, повторите позже |
Повторять имеет смысл 408, 429 и 5xx. Остальные 4xx — это вердикт: тот же запрос не пройдёт и со второй попытки.
Что списывается с квоты
Квота считается в событиях. Профили пользователей (users[], а также те, что Metriox достаёт из самих событий) не списываются, если приехали вместе с событиями — вы уже платите за действие, которое раскрыло личность, и одно обновление Telegram не должно стоить дважды.
Профиль, отправленный отдельно, стоит одно событие. Практический вывод: держите users[] в том же запросе, что и события, — так и дешевле, и меньше запросов.
Запрос вообще без событий отклоняется (400), поэтому загрузить свою базу пользователей одним users[] нельзя: профиль появляется вместе с первым действием человека.
Что дальше?
- SDK — рекомендуемый способ интеграции
- События — структура событий
- Типы данных — какие типы свойств поддерживаются