Верификация номера через Telegram OTP в Next.js: App Router за 20 минут
Интеграция
10 июля 2026 · 8 мин

Верификация номера через Telegram OTP в Next.js (App Router)

Telegram OTP присылает код подтверждения прямо в мессенджер через официальный Telegram Gateway — без SMS и без собственных ботов. Соберём рабочую верификацию номера на Next.js App Router: два серверных Route Handler и один клиентский компонент. Секрет при этом остаётся на бэкенде.

Telegram OTP — метод верификации, при котором одноразовый код приходит пользователю в Telegram. Verificahub отправляет его через официальный сервис Telegram Gateway («Verification Codes»), поэтому вам не нужны собственные боты, номера или отдельный договор с Telegram — достаточно одного вызова API.

В этом гайде соберём верификацию на Next.js (App Router). Логика делится на две части: серверные Route Handlers, которые ходят в api.verificahub.ru с секретом, и клиентский компонент с двумя шагами — ввод номера и ввод кода. Ключевое правило: api_secret никогда не покидает сервер.

Как устроен метод telegram_otp

В отличие от обратного flash-call, который подтверждается автоматически по звонку, Telegram OTP работает как классический одноразовый пароль: пользователь читает цифры в мессенджере и вводит их в форму. Поэтому весь поток — это два запроса к API.

  1. POST /v1/verify с method: "telegram_otp" инициирует отправку. В ответ приходят request_id и code_length (сколько цифр в коде). Сам код API не возвращает — его видит только пользователь в Telegram.
  2. POST /v1/verify/check с request_id и введённым code сверяет код. При успехе возвращает status: "verified" и маскированный номер.

api_secret — это пароль от вашего аккаунта. Внешний API строго server-to-server: держите ключи в переменных окружения и вызывайте Verificahub только из Route Handlers, никогда из клиентского кода или мобильного приложения.

Что понадобится

  • Проект на Next.js 13.4+ с App Router (директория app/).
  • Пара ключей api_key / api_secret из личного кабинета и включённый на аккаунте метод telegram_otp.
  • Переменные окружения VH_KEY и VH_SECRET в .env.local.
  • Номера пользователей в формате E.164: российские +7….

Метод telegram_otp включается на аккаунте по запросу — пока он не активирован, запросы этим методом не пройдут; напишите в поддержку, чтобы открыть канал. Если Telegram недоступен для конкретного номера, POST /v1/verify вернёт delivery_unavailable — на этот случай стоит держать запасной способ подтверждения.

Шаг 1. Старт проверки: app/api/verify/start/route.ts

Первый обработчик принимает номер от клиента и вызывает POST /v1/verify. Авторизация — HTTP Basic: api_key как логин и api_secret как пароль, склеенные через двоеточие и закодированные в base64. Route Handlers по умолчанию исполняются в Node-рантайме, поэтому Buffer доступен.

js
// app/api/verify/start/route.ts
import { NextResponse } from 'next/server'

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

export async function POST(req: Request) {
  const { phone } = await req.json()

  const r = await fetch(`${API}/verify`, {
    method: 'POST',
    headers: { Authorization: AUTH, 'Content-Type': 'application/json' },
    body: JSON.stringify({ phone_number: phone, method: 'telegram_otp' }),
  })
  const data = await r.json()
  if (!r.ok) return NextResponse.json(data, { status: r.status })

  // клиенту отдаём только id и длину кода — стоимость и служебные поля ему не нужны
  return NextResponse.json({
    request_id: data.request_id,
    code_length: data.code_length,
  })
}

Ответ 201 Created содержит request_id и code_length. Возвращаем клиенту только эти два поля.

json
{
  "method": "telegram_otp",
  "request_id": "a1b2c3d4",
  "phone_number": "+79991234567",
  "status": "sent",
  "cost": { "amount": 0.90, "currency": "RUB" },
  "expires_at": "2026-07-10T09:35:00Z",
  "code_length": 6
}

Шаг 2. Проверка кода: app/api/verify/check/route.ts

Второй обработчик принимает request_id и введённый пользователем код и передаёт их в POST /v1/verify/check. Важно аккуратно пробросить и статус, и тело ответа обратно на клиент — в нём лежит как успех, так и коды ошибок.

js
// app/api/verify/check/route.ts
import { NextResponse } from 'next/server'

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

export async function POST(req: Request) {
  const { request_id, code } = await req.json()

  const r = await fetch(`${API}/verify/check`, {
    method: 'POST',
    headers: { Authorization: AUTH, 'Content-Type': 'application/json' },
    body: JSON.stringify({ request_id, code }),
  })
  const data = await r.json()
  // 200 -> { status: 'verified', phone_number },
  // иначе problem+json со стабильным error_code
  return NextResponse.json(data, { status: r.status })
}

При верном коде приходит 200 OK со status: "verified" и маскированным номером:

json
{ "status": "verified", "phone_number": "+7999*****67" }

При неверном коде — 400 с error_code: "invalid_code" и полем attempts_remaining: сколько попыток осталось до блокировки сессии.

json
{
  "type": "https://docs.verificahub.ru/errors/invalid_code",
  "title": "Invalid code",
  "status": 400,
  "error_code": "invalid_code",
  "detail": "The code you entered is incorrect.",
  "attempts_remaining": 2
}

Шаг 3. Клиентский компонент: номер → код

Компонент помечен 'use client' и хранит два состояния: текущий шаг и данные формы. Он общается только с вашими Route Handlers (/api/verify/*), а не с Verificahub напрямую. Поле для кода ограничиваем по code_length, который вернул сервер.

js
'use client'
import { useState } from 'react'

export function VerifyForm() {
  const [step, setStep] = useState('phone') // 'phone' | 'code'
  const [phone, setPhone] = useState('+7')
  const [code, setCode] = useState('')
  const [requestId, setRequestId] = useState('')
  const [codeLength, setCodeLength] = useState(6)
  const [error, setError] = useState('')

  async function start() {
    setError('')
    const r = await fetch('/api/verify/start', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ phone }),
    })
    const data = await r.json()
    if (!r.ok) return setError('Не удалось отправить код в Telegram')
    setRequestId(data.request_id)
    setCodeLength(data.code_length)
    setStep('code')
  }

  async function check() {
    setError('')
    const r = await fetch('/api/verify/check', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ request_id: requestId, code }),
    })
    const data = await r.json()
    if (r.ok && data.status === 'verified') {
      // номер подтверждён — продолжаем сценарий (регистрация, вход)
      return
    }
    if (data.error_code === 'invalid_code') {
      setError(`Неверный код. Осталось попыток: ${data.attempts_remaining}`)
    } else {
      setError('Ошибка проверки, попробуйте ещё раз')
    }
  }

  if (step === 'phone') {
    return (
      <form onSubmit={(e) => { e.preventDefault(); start() }}>
        <input value={phone} onChange={(e) => setPhone(e.target.value)}
               inputMode="tel" placeholder="+79991234567" />
        <button type="submit">Получить код в Telegram</button>
        {error && <p role="alert">{error}</p>}
      </form>
    )
  }

  return (
    <form onSubmit={(e) => { e.preventDefault(); check() }}>
      <input value={code} onChange={(e) => setCode(e.target.value)}
             inputMode="numeric" maxLength={codeLength}
             placeholder={'•'.repeat(codeLength)} />
      <button type="submit">Подтвердить</button>
      {error && <p role="alert">{error}</p>}
    </form>
  )
}

Клиент не знает ни api_key, ни api_secret, ни даже адреса api.verificahub.ru. Он видит только /api/verify/start и /api/verify/check — это и есть правильная граница безопасности.

Обработка ошибок

Ошибки Verificahub возвращает в формате RFC-7807 (application/problem+json) со стабильным полем error_code. Ветвитесь по нему, а не по человекочитаемому detail, который может меняться. При неудачной инициации деньги не списываются.

error_codeHTTPЧто делать
invalid_code400Показать attempts_remaining и дать повторить ввод.
validation_error400Код должен быть 4–8 цифр, номер — в формате E.164 +7….
not_pending409Сессия уже подтверждена, истекла или провалена — начните новую.
not_found404Неверный request_id или он не принадлежит аккаунту.
delivery_unavailable400Telegram недоступен для номера — переключитесь на запасной канал.
insufficient_balance402Пополните баланс в кабинете.
rate_limit_exceeded429Учтите заголовок Retry-After и снизьте частоту запросов.

Нужны ли здесь вебхуки

При Telegram OTP результат вы получаете синхронно — прямо в ответе POST /v1/verify/check. Поэтому опрашивать статус или ждать вебхук, как при reverse flash call, не требуется: check сразу говорит verified или invalid_code.

Вебхуки тут — необязательное дополнение. События verification.delivered и verification.verified (подпись HMAC-SHA256, заголовок X-Verificahub-Signature) удобно использовать для аудита и аналитики доставки. Настраиваются они в кабинете; детали — в документации API.

Стоимость и оплата за результат

Telegram OTP стоит от 0,90 ₽ за подтверждение. Модель — оплата за результат (charge-on-delivery): списание происходит за доставленный код, а неудачная инициация не тарифицируется. Подписок и пакетов нет — вы платите только за факт доставки. Полное сравнение методов и актуальные цены смотрите на странице тарифов.

Что дальше: запасной канал

Не у всех пользователей есть Telegram. Чтобы не терять конверсию, добавьте запасной канал: если POST /v1/verify вернул delivery_unavailable, переключитесь на обратный flash-call или SMS. Как выстроить автоматическую цепочку «Telegram → звонок → SMS» на своей стороне, разбираем в гайде про каскадную доставку OTP.

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

Возвращает ли API сам код подтверждения?

Нет. Код Verificahub не возвращает никогда — его видит только пользователь в Telegram. Ваш сервер получает лишь request_id и code_length, а сверку делает POST /v1/verify/check.

Нужен ли собственный Telegram-бот?

Нет. Код отправляется через официальный Telegram Gateway от сервиса «Verification Codes». Ботов, номеров и отдельного договора с Telegram не требуется — вы вызываете только API Verificahub.

Что делать при неверном коде?

Ответ 400 с error_code: "invalid_code" содержит поле attempts_remaining. Покажите пользователю, сколько попыток осталось, и дайте повторно ввести код. Когда попытки закончатся, начните новую проверку через POST /v1/verify.

Почему нельзя вызывать API прямо из клиента?

Для авторизации нужен api_secret — это пароль от аккаунта, и в браузере или мобильном приложении его украдут. Все вызовы идут через серверные Route Handlers, а ключи лежат в переменных окружения.

Сколько действует код?

По умолчанию 300 секунд; задаётся полем expiry_seconds (диапазон 1–600) в POST /v1/verify. У кодов Telegram Gateway короткий TTL, поэтому большое значение ставить не стоит.

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

Подключите Telegram OTP

Код в мессенджер через официальный Telegram Gateway, от 0,90 ₽ за подтверждение, оплата только за доставленный код. Ключи — в личном кабинете.