Каскадная доставка OTP: fallback-цепочка звонок → Telegram → SMS на Node.js
Интеграция
18 июля 2026 · 7 мин

Каскадная доставка OTP: собираем fallback-цепочку каналов

Ни один канал доставки OTP не даёт 100% доходимости: у кого-то нет Telegram, кто-то не может позвонить, где-то не проходит SMS. Каскад — это цепочка каналов с переходом к следующему при неудаче. Разберём, как собрать её поверх API Verificahub: порядок каналов, функцию sendWithCascade на Node.js и почему неудачные попытки в каскаде не тарифицируются.

Каскадная доставка OTP — это стратегия, при которой код (или подтверждение) отправляется не одним фиксированным способом, а по цепочке каналов: если первый не сработал, автоматически подключается второй, затем третий. Цель двойная — поднять доходимость (кто-то из пользователей всегда выпадает из любого одного канала) и снизить стоимость (дешёвый канал закрывает большинство, дорогой подстраховывает остаток).

Сразу честно: в Verificahub нет «автокаскада» на стороне API. Одна сессия POST /v1/verify использует ровно один method. Каскад вы собираете в своём коде — последовательными вызовами с разными методами и переходом к следующему каналу при неудаче инициации или доставки. Ниже — рабочая схема и код.

Весь каскад живёт на бэкенде. Внешний API — server-to-server, api_secret — это пароль: не встраивайте его в браузер или мобильное приложение и не проксируйте на клиент. Фронтенд общается только с вашим сервером.

Что такое каскадная доставка OTP

Определение и общий разбор термина — в глоссарии: каскадная доставка. Практический смысл прост: у каждого канала своя доходимость и цена. Обратный flash-call бесшовный и самый дешёвый, но требует, чтобы пользователь мог позвонить (иногда мешает корпоративная АТС). Telegram отлично доходит, но только если у человека установлен мессенджер. SMS доходит почти всегда, но дороже и медленнее. Каскад комбинирует их сильные стороны.

Дешёвый канал закрывает большинство пользователей, а запасной подстраховывает остальных — так каскад одновременно повышает доходимость и держит среднюю цену подтверждения низкой.

Почему в Verificahub нет автокаскада — и это нормально

Ответ POST /v1/verify дискриминирован по method: для reverse_flash_call приходит number_to_call (пользователь сам звонит, сессия подтверждается по Caller ID автоматически), для telegram_otpcode_length (код читается в Telegram и проверяется через POST /v1/verify/check). Смешивать методы в одной сессии нельзя — и это удобно: вы полностью контролируете логику перехода и не зависите от «магии» на стороне API.

Переключаться на следующий канал имеет смысл в двух точках:

  • На инициацииPOST /v1/verify вернул ошибку, означающую, что канал недоступен прямо сейчас (например no_gateway_available, delivery_unavailable для Telegram, provider_error).
  • На доставке/подтверждении — сессия начата, но закончилась статусом expired (истёк TTL) или failed; либо пользователь сам нажал «получить код другим способом».

Как выбрать порядок каналов

Базовый принцип — сначала самый дешёвый и бесшовный, потом запасные. Так вы платите за дорогой канал только тогда, когда дешёвый реально не сработал.

  • Первым — reverse_flash_call: не нужно вводить код, самый дешёвый метод, от 0,25 ₽.
  • Вторым — telegram_otp: высокая доходимость там, где есть мессенджер, от 0,90 ₽ (официальный Telegram Gateway).
  • Последним — sms: универсальный запасной канал, доходит почти везде.
КаналКак подтверждаетсяСтоимостьКогда переходить дальше
reverse_flash_callПользователь звонит, авто-подтверждение по Caller IDот 0,25 ₽Инициация вернула no_gateway_available/provider_error, либо статус стал expired/failed
telegram_otpКод в Telegram, проверка через /v1/verify/checkот 0,90 ₽delivery_unavailable при инициации или истёк TTL без верного кода
smsКод по SMS, проверка через /v1/verify/checkпо тарифуПоследний рубеж: если тоже expired/failed — ручная проверка или поддержка

Порядок подстраивайте под аудиторию: если ваши пользователи активно сидят в Telegram, поставьте telegram_otp первым (метод нужно заранее включить на аккаунте). SMS OTP мы разворачиваем на собственной сети +7 SIM-шлюзов — уточняйте доступность и цену для вашего аккаунта.

Когда переходить к следующему каналу

Ошибки приходят в формате RFC-7807 со стабильным полем error_codeветвитесь по нему, а не по тексту `detail`. Не любую ошибку стоит «лечить» сменой канала: неверный номер или пустой баланс новый канал не спасёт.

error_codeЧто означаетДействие в каскаде
no_gateway_availableНет свободного канала для методаПерейти к следующему каналу
delivery_unavailableTelegram недоступен для этого номераПерейти к следующему каналу (звонок / SMS)
provider_errorВременный сбой провайдераСледующий канал или повтор с backoff
rate_limit_exceededСлишком часто (429)Канал не менять — выждать Retry-After
insufficient_balanceНедостаточно средствОстановиться и пополнить баланс
validation_errorНомер не в формате E.164 +7…Остановиться и исправить ввод

Второй триггер — жизненный цикл сессии sent → delivered → verified | expired | failed. Если после инициации статус дошёл до expired или failed, это сигнал эскалировать на следующий канал. Узнавать статус лучше через webhook (события verification.*, подпись HMAC-SHA256), а не опросом.

Функция sendWithCascade на Node.js

Собираем ядро каскада — инициацию с переходом по каналам. Ключи читаем из окружения, авторизация — HTTP Basic. Функция возвращает первую успешно начатую сессию либо бросает исключение, если варианты кончились.

SMS-OTP (метод sms) в Verificahub подключается поэтапно через собственную SIM-сеть — уточните доступность для вашего аккаунта. Пока SMS не включён, используйте каскад из двух каналов: reverse_flash_calltelegram_otp.

js
import 'dotenv/config'

const API = 'https://api.verificahub.ru/v1'
const AUTH =
  'Basic ' + Buffer.from(`${process.env.VH_KEY}:${process.env.VH_SECRET}`).toString('base64')

// Порядок каскада: сначала самый дешёвый и бесшовный, затем запасные.
const CASCADE = ['reverse_flash_call', 'telegram_otp', 'sms']

// error_code, при которых канал недоступен «прямо сейчас» — берём следующий.
const FALLBACK_CODES = new Set([
  'no_gateway_available',
  'delivery_unavailable',
  'provider_error',
  'pricing_unavailable',
])

async function initiate(phone, method) {
  const res = await fetch(`${API}/verify`, {
    method: 'POST',
    headers: { Authorization: AUTH, 'Content-Type': 'application/json' },
    body: JSON.stringify({ phone_number: phone, method, expiry_seconds: 180 }),
  })
  return { ok: res.ok, data: await res.json() }
}

// Инициируем проверку, спускаясь по каскаду. Возвращаем первую начатую сессию.
async function sendWithCascade(phone, channels = CASCADE) {
  let lastProblem
  for (const method of channels) {
    const { ok, data } = await initiate(phone, method)
    if (ok) return { method, ...data } // { method, request_id, number_to_call?, code_length?, ... }

    lastProblem = data
    // «Чинимые» ошибки (неверный номер, нет баланса, rate limit) — каскад не поможет.
    if (!FALLBACK_CODES.has(data.error_code)) break
    // Иначе канал недоступен — пробуем следующий. Неудачная инициация не тарифицируется.
  }
  throw Object.assign(new Error('cascade exhausted'), { problem: lastProblem })
}

export { sendWithCascade, CASCADE }

Дальше действуйте по методу вернувшейся сессии: для reverse_flash_call покажите пользователю number_to_call, для telegram_otp/sms — попросите ввести код и проверьте его через POST /v1/verify/check.

Эскалация при неудачной доставке

Если канал начался, но не привёл к подтверждению (истёк TTL, статус expired/failed, или пользователь попросил другой способ), переходим на оставшиеся каналы — снова через sendWithCascade, но уже с «хвостом» списка.

js
// Каналы после текущего — для перехода при expired/failed или по кнопке «другой способ».
const nextChannels = (current) => CASCADE.slice(CASCADE.indexOf(current) + 1)

// Проверка кода — только для telegram_otp и sms.
// reverse_flash_call подтверждается автоматически, /v1/verify/check ему не нужен.
async function check(requestId, code) {
  const res = await fetch(`${API}/verify/check`, {
    method: 'POST',
    headers: { Authorization: AUTH, 'Content-Type': 'application/json' },
    body: JSON.stringify({ request_id: requestId, code }),
  })
  return { ok: res.ok, data: await res.json() } // 200 { status:'verified' } | 400 invalid_code
}

// Эскалация: сессия истекла/провалилась — берём следующие каналы.
async function escalate(phone, currentMethod) {
  const rest = nextChannels(currentMethod)
  if (rest.length === 0) return null // каналы кончились — показываем поддержку
  return sendWithCascade(phone, rest)
}

Не запускайте эскалацию, пока не убедились, что текущий канал точно провалился. Для reverse_flash_call дождитесь webhook verification.verified или статуса verified через GET /v1/verify/{request_id} — иначе рискуете зря запустить и оплатить платный SMS, пока пользователь ещё звонит.

Детали интеграции каждого канала разобраны отдельно: верификация по звонку на Node.js и Telegram-верификация на Next.js. Каскад просто оркеструет то, что описано в этих гайдах.

Биллинг: сколько на самом деле стоит перебор каналов

Модель оплаты — за результат, со списанием при доставке (charge-on-delivery): неудачная инициация не тарифицируется. Поэтому переход к следующему каналу после ошибки инициации (no_gateway_available, delivery_unavailable, provider_error) бесплатен — за канал, который даже не начал доставку, вы ничего не платите.

Списание происходит в момент доставки, а не подтверждения. Если канал уже доставил код или звонок, он будет оплачен, даже если пользователь так и не подтвердился (expired/failed) — и следующий канал, если вы эскалируете, тоже. Отсюда правило порядка: дешёвый бесшовный канал первым удерживает среднюю стоимость подтверждения минимальной, а за дорогой Telegram или SMS вы платите только когда до них реально дошла очередь. Актуальные цены по каналам — на странице тарифов.

Коротко

  • Автокаскада в API нет: одна сессия = один method, цепочку строите в своём коде.
  • Порядок — от дешёвого и бесшовного к универсальному: reverse_flash_call → telegram_otp → sms.
  • Переходите к следующему каналу по error_code инициации или по статусу expired/failed, а не по тексту detail.
  • Оплата за результат со списанием при доставке: неудачная инициация бесплатна, но уже доставленный канал тарифицируется — поэтому дешёвый бесшовный канал ставим первым.

Частые вопросы

Есть ли в Verificahub автоматический каскад на стороне API?

Нет. Одна сессия POST /v1/verify использует ровно один method. Каскад вы собираете сами — последовательными вызовами с разными методами и переходом к следующему каналу при неудаче инициации или доставки.

По каким сигналам переходить к следующему каналу?

По error_code при инициации (no_gateway_available, delivery_unavailable, provider_error) и по финальному статусу сессии expired/failed. Ветвитесь по стабильному error_code, а не по тексту detail.

Платится ли отдельно за каждый канал в каскаде?

Оплата — за результат со списанием при доставке (charge-on-delivery): неудачная инициация не тарифицируется, поэтому переход к следующему каналу после ошибки инициации бесплатен. Но если канал уже доставил код или звонок, он будет оплачен, даже если подтверждение не состоялось. Поэтому дешёвый бесшовный канал ставят первым — так средняя цена подтверждения остаётся минимальной.

Какой канал ставить первым?

Обычно самый дешёвый и бесшовный — reverse flash call (от 0,25 ₽). Если аудитория активно пользуется Telegram, первым можно поставить Telegram OTP. Универсальный запасной канал — SMS.

Запуск за 24 часа

Соберите каскад на одном API

Один ключ, оплата за результат, каналы от 0,25 ₽. Reverse flash call, Telegram и SMS — переключайтесь между ними в своём коде.