ДокументацияНастройка проекта
Отправка событий
«Интеграции» → «Отправка событий»: укажите адрес своей системы, выберите типы событий — и TERVIO будет сам присылать POST с JSON: кто вошёл в воронку, какие блоки прошёл, что ответил, перешёл ли по ссылке, кто оплатил, на каком этапе сделка. Блок «Вебхук» в каждую воронку ставить не нужно — он остаётся для случаев, когда данные нужны посреди конкретной воронки (Блоки).
Как подключить
- «Интеграции» → «Отправка событий» → «Добавить адрес». Адрес — полный, с
https://: адрес приёмника вашей аналитики, n8n (Webhook), Make, своего сервера. Адреса внутренней сети (localhost, 10.x, 192.168.x) и адреса самого TERVIO не принимаются. - Выберите, как отправлять: пачкой — до 100 событий одним запросом раз в несколько секунд (по умолчанию, проще для аналитики и легче серверу) или по одному — отдельный запрос на каждое событие.
- Отметьте типы событий. «Клиент прошёл блок» — самое частое: у крупного проекта это тысячи в час.
- Сохраните и скопируйте секрет подписи — TERVIO показывает его один раз (ниже — как проверять).
- Нажмите «Отправить тестовое событие»: TERVIO сразу пошлёт событие
testи покажет, что ответил адрес.
Адресов может быть до пяти: например, аналитика получает всё, а n8n — только оплаты. Ваш сервер должен ответить кодом 2xx не дольше чем за 10 секунд — тогда отправка засчитана.
Формат
Метод — POST, тело — JSON в UTF-8, заголовки: Content-Type: application/json, X-Tervio-Signature (подпись), X-Tervio-Event-Count (сколько событий в запросе), User-Agent: TERVIO-Events/1.0. Пачкой тело — объект со списком событий:
{
"school": "myproject",
"sent_at": "2026-10-19T09:12:08.004+00:00",
"count": 2,
"events": [ { …событие… }, { …событие… } ]
}По одному тело — само событие. Поля события (если у события поля нет, оно не передаётся):
- id — номер события: у каждого события свой, у одного и того же — всегда один. Повтор доставки, «Отправить заново» и то же событие из выгрузки истории приходят с тем же id — по нему отсеивайте повторы. Повторный проход воронки — это новый прогон: у его старта, шагов и конца новые id, прошлые события не перезаписываются.
- type — тип события, список ниже.
- occurred_at — когда случилось, ISO 8601 в UTC.
- school — адрес проекта в TERVIO (слаг).
- history — есть и равно true только у событий из выгрузки истории.
- client — кто клиент:
id— номер клиента в TERVIO (тот же, что {tervio_id} в воронке),messenger(telegram, vk, max, instagram, avito, whatsapp, cabinet),messenger_id— id в мессенджере,bot— бот или номер проекта, через который идёт переписка (у бота ещёfields— его поля из «Ботов» → «Поля бота»: комната, дата вебинара и т. п., ключ → значение; числа приходят числами, «да / нет» — true / false;type— ключ типа бота из «Типов ботов»),username,name,phone. Имя и телефон — только если TERVIO их знает. - scenario — воронка:
id,name,run_id— номер прогона (один проход клиента по воронке; по нему собираются старт, шаги, ответы и конец одного прохода),version— номер версии листа в момент события: после каждого сохранения схемы он растёт на 1. Каким был лист и текст блока в этой версии, видно в «Истории версий» листа (меню «⋯» у листа): там тот же номер. История хранит последние 20 версий и всё за 30 дней. - block — блок воронки:
id,type,name— подпись блока на холсте. id блока не меняется, когда блок правят: правку видно поscenario.version. - data — данные самого события — у каждого типа свои.
События и примеры
Пример целиком — «Клиент вошёл в воронку» (funnel.started): лист и старт, ссылка запуска, метки UTM и параметры старта.
{
"id": "myproject.run1532.started",
"type": "funnel.started",
"occurred_at": "2026-10-19T09:12:03.120+00:00",
"school": "myproject",
"client": {
"id": 8812,
"messenger": "telegram",
"messenger_id": "512334455",
"bot": {
"id": 3, "kind": "bot", "platform": "telegram", "name": "Бот проекта", "username": "myproject_bot",
"type": "живой_вебинар",
"fields": { "комната": "Б-12", "дата_вебинара": "2026-10-28 19:00" }
},
"username": "ivan",
"name": "Иван",
"phone": "+79001234567"
},
"scenario": { "id": 12, "name": "Прогрев", "run_id": 1532, "version": 37 },
"block": { "id": "start_a1b2", "type": "start", "name": "Старт" },
"data": {
"start": { "id": "start_a1b2", "trigger": "bot_start", "name": "Старт" },
"start_tag": "reels",
"launch_link": { "code": "okt", "title": "Реклама в октябре" },
"utm": { "utm_source": "vk", "utm_campaign": "okt" },
"params": { "price": "5000" }
}
}У остальных — что лежит в data (и в block, где он важен):
Клиент прошёл блок — funnel.block
"block": { "id": "message_9f3c", "type": "message", "name": "Приветствие" },
"data": { "step_type": "send_message" }Клиент ответил в воронке — funnel.reply
"data": {
"text": "want_course",
"kind": "текст",
"source": "button",
"message_id": 99812,
"saved_to": ["ответ"],
"button": { "text": "Хочу на курс", "value": "want_course" }
}Переход по отслеживаемой ссылке — funnel.link_click
"data": { "url": "https://example.ru/pay", "label": "Оплата", "clicks": 1 }
// ссылка из рассылки — без block, номер рассылки в data:
"data": { "url": "https://example.ru/pay", "label": "Оплата", "clicks": 1, "broadcast_id": 14,
"broadcast": "Октябрьская акция", "variant": "B" }Блок «ИИ» выбрал выход — funnel.ai_exit
"data": { "exit": "agreed", "exit_title": "Согласился", "reason": "готов купить тариф «Про»", "has_arrow": true }Воронка закончилась или остановлена — funnel.finished
"data": {
"status": "stopped",
"reason": "client_reply",
"reason_text": "Остановлена: клиент ответил, дальше ведёт человек."
}Клиент нажал кнопку бота — button.pressed
"block": { "id": "message_9f3c", "type": "message", "name": "Какой курс?" },
"data": { "text": "Хочу на курс", "value": "want_course", "message_id": 99813 }Клиент написал сообщение — message.received
"data": {
"message_id": 99815,
"text": "Здравствуйте, сколько стоит курс?",
"chat": "private"
}Новый клиент — client.new
"client": { … } — поля data нетКлиент дошёл до этапа — stage.reached
"data": { "code": "form_submitted", "title": "Сдал анкету", "by": "scenario" }Сделка перешла на этап — deal.stage
"data": { "code": "taken", "title": "Взят в работу", "previous": null, "by": "Мария" }Тег поставлен или снят — tag.added / tag.removed
"data": { "tag_id": 5, "tag": "горячий" }Оплата прошла, не прошла, возврат — payment.paid / payment.failed / payment.refunded
"data": {
"order_id": "tv-381",
"status": "paid",
"product": "Курс «Старт»",
"amount": "29900.00",
"currency": "RUB",
"provider": "prodamus"
}Сообщение рассылки отправлено — broadcast.sent
"data": {
"broadcast_id": 14,
"broadcast": "Октябрьская акция",
"via": "message",
"variant": "B",
"ab": true
}Сообщение рассылки не доставлено — broadcast.failed
"data": {
"broadcast_id": 14,
"broadcast": "Октябрьская акция",
"status": "blocked",
"reason": "Человек остановил бота — сообщение не доставлено. Бот снова сможет писать ему, когда человек сам напишет боту."
}Тестовое событие — test
"data": { "message": "Тестовое событие TERVIO: адрес принимает данные…" }- funnel.block — блоки, которые попадают в журнал прогона: отправили сообщение, медиа или голосовое, сделали действие с чатом, «Вычислить», пропуск по расписанию. Переменные, вебхуки, развилки и переходы своего события не дают. Стартовый блок без текста — тоже: его заменяет «Клиент вошёл в воронку».
- funnel.reply — ответ блоку, который ждал ответа.
text— что пришло в воронку (у кнопки бота это её значение),source— откуда ответ: button (нажал кнопку), text (написал сам), media (прислал вложение);button— подпись и значение нажатой кнопки бота Telegram,saved_to— в какие переменные ответ сохранён. Вход в чат и переход по ссылке ответом не считаются — у них свои события. - message.received — любое входящее сообщение клиента — боту или на рабочий номер, в личке (
chat: private) и в группе (group), с воронкой и без.media— вид вложения (image, video, voice, document…),forwarded— сообщение переслано. Нажатие кнопки сюда не входит — этоbutton.pressed. В проекте с живой перепиской таких событий много. - funnel.link_click —
clicks— номер перехода по этой ссылке (1 — первый). Ссылка из рассылки: безblock, в data —broadcast_id,broadcastи вариант A/B-тестаvariant. - funnel.ai_exit — выход блока «ИИ»: выбранный нейросетью («Согласился», «Отказался») или системный — «Нужен человек», «Молчит», «Лимит ответов», «Ошибка».
has_arrow— есть ли у выхода стрелка дальше. - funnel.finished —
status: done — дошла до конца, stopped — остановлена, failed — сломалась (тогда естьerror).reason— код причины,reason_text— она же словами. - payment.* — заказы эквайринга TERVIO.
status: paid, denied, canceled, refunded. Тестовые оплаты приходят с"test": true. - button.pressed — нажатие кнопки под сообщением бота (Telegram, MAX, ВКонтакте).
text— подпись,value— значение кнопки, если оно другое. Кнопка была в воронке — естьscenarioиblock; в рассылке —broadcast_id,broadcastи вариант A/B-тестаvariant. - broadcast.sent —
via: message — человек получил сообщение рассылки, funnel — вошёл в воронку рассылки (её шаги придут своими событиями). У рассылки с A/B-тестом ещёab: trueиvariant— какой вариант получил человек (A, B…);ab_rest: true— он получил лучший вариант уже после теста, а не участвовал в нём. У обычной рассылки этих полей нет. - broadcast.failed —
status: failed — площадка не приняла сообщение, blocked — человек остановил бота.reason— причина словами. У A/B-теста — те жеab,variantиab_rest, что у broadcast.sent. «Доставлено» TERVIO не присылает: Telegram о доставке не сообщает.
Подпись
Заголовок X-Tervio-Signature: sha256=<hex> — HMAC-SHA256 тела запроса по секрету адреса. Считайте его от байтов тела ровно в том виде, в каком они пришли: разобранный и собранный заново JSON даст другую подпись. Сравнивайте за постоянное время (hmac.compare_digest, crypto.timingSafeEqual).
Python:
import hashlib, hmac, json
SECRET = "tvs_…" # секрет адреса из TERVIO
def is_from_tervio(body: bytes, header: str | None) -> bool:
expected = "sha256=" + hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header or "")
# FastAPI
@app.post("/tervio")
async def tervio(request: Request):
body = await request.body() # байты как пришли, без разбора
if not is_from_tervio(body, request.headers.get("X-Tervio-Signature")):
raise HTTPException(401)
data = json.loads(body)
for event in data.get("events", [data]): # пачкой — список, по одному — само событие
... # event["id"] — отсеивайте повторы
return {"ok": True}JavaScript (Node.js, Express):
const crypto = require('crypto')
const SECRET = process.env.TERVIO_SECRET // секрет адреса из TERVIO
// express.raw — подпись считается от байтов тела, а не от разобранного JSON
app.post('/tervio', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + crypto.createHmac('sha256', SECRET).update(req.body).digest('hex')
const got = req.get('X-Tervio-Signature') || ''
const ok = got.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))
if (!ok) return res.status(401).end()
const data = JSON.parse(req.body.toString('utf8'))
for (const event of data.events || [data]) {
// event.id — отсеивайте повторы
}
res.json({ ok: true })
})«Выпустить новый секрет» на странице адреса меняет секрет сразу: старый перестаёт действовать, и пока вы не замените его у себя, проверка будет отклонять события. Целиком секрет показывается один раз.
Повторы, пауза, хранение
- Очередь — событие записывается в очередь TERVIO вместе с самим действием — не потеряется, даже если ваш сервер недоступен. События одного адреса уходят по порядку.
- Повторы — адрес не ответил 2xx за 10 секунд — те же события уйдут ещё раз с нарастающей паузой: 30 секунд, 1, 2, 5, 10, 20, 40 минут, 1, 2, 4, 6, 8 часов. Повтор приходит с теми же id.
- Пауза — 13 неудач подряд (почти сутки) — отправка встаёт на паузу, а подписанным на «Отправка событий на паузе» приходит уведомление (Уведомления). События продолжают копиться; «Возобновить отправку» на странице адреса отправит их по порядку.
- Хранение — очередь и журнал доставок хранятся 7 дней. Что старше — удаляется, в том числе неотправленное.
- Журнал — вкладка «Журнал доставок» на странице адреса: когда, что ушло, что ответил адрес. У неудачной отправки — «Отправить заново». Журнал обновляется сам.
- Выключить — пока отправка выключена, события не копятся. Включите — пойдут только новые; пропущенное соберёт «Выгрузить историю».
Выгрузка истории
«Выгрузить историю» на странице адреса отправляет события за выбранный период — не дальше 30 дней назад, — собранные из того, что TERVIO уже хранит. Они идут в ту же очередь порциями, с "history": true, и не задерживают новые события. У большинства событий id тот же, что у живых, — повторы можно отсеять.
- Целиком — старт воронки, шаги, конец воронки (время — последнее изменение прогона), сообщения клиентов, новый клиент, этапы, оплаты и возвраты, рассылки. Номера версии листа у событий из истории нет.
- Частично — ответы клиента — только те, на которые воронка отреагировала, без блока и кнопки; переходы по ссылкам — только первый переход по каждой ссылке; выходы блока «ИИ» — название выхода без ключа; этап сделки — только текущий; теги — только те, что стоят на чатах сейчас; «Оплата не прошла» — со временем последнего изменения заказа.
- Нельзя — «Тег снят»: TERVIO не хранит, когда тег сняли.
Пока идёт выгрузка, в очереди адреса не больше пары тысяч событий: история уходит в паузах между новыми. Крупный проект за 30 дней — сотни тысяч шагов воронок; выгрузка займёт около часа.