ДокументацияНастройка проекта

Отправка событий

для администратора школы

«Интеграции» → «Отправка событий»: укажите адрес своей системы, выберите типы событий — и TERVIO будет сам присылать POST с JSON: кто вошёл в воронку, какие блоки прошёл, что ответил, перешёл ли по ссылке, кто оплатил, на каком этапе сделка. Блок «Вебхук» в каждую воронку ставить не нужно — он остаётся для случаев, когда данные нужны посреди конкретной воронки (Блоки).

Как подключить

  1. «Интеграции» → «Отправка событий» → «Добавить адрес». Адрес — полный, с https://: адрес приёмника вашей аналитики, n8n (Webhook), Make, своего сервера. Адреса внутренней сети (localhost, 10.x, 192.168.x) и адреса самого TERVIO не принимаются.
  2. Выберите, как отправлять: пачкой — до 100 событий одним запросом раз в несколько секунд (по умолчанию, проще для аналитики и легче серверу) или по одному — отдельный запрос на каждое событие.
  3. Отметьте типы событий. «Клиент прошёл блок» — самое частое: у крупного проекта это тысячи в час.
  4. Сохраните и скопируйте секрет подписи — TERVIO показывает его один раз (ниже — как проверять).
  5. Нажмите «Отправить тестовое событие»: 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 дней — сотни тысяч шагов воронок; выгрузка займёт около часа.