Разработка · 7 мин чтения

Next.js лендинг с онлайн-оплатой Т‑Банк: пошаговая интеграция эквайринга

Разработка next.js лендинга с онлайн оплатой tbank почти никогда не упирается во фронтенд - там всё тривиально: кнопка, fetch, редирект. Ломается интеграция на стыке API эквайринга и вебхуков, и именно туда уходит девяносто процентов времени в проектах, которые я делаю по этой теме. Ниже - рабочая схема от заявки на терминал в Т‑Банке до проверки токена в уведомлении, с кодом на Next.js App Router, который можно брать почти без правок под свой лендинг.

Как работает next.js лендинг с онлайн оплатой tbank на уровне схемы

Пользователь жмёт «Оплатить» на лендинге, фронтенд дёргает ваш собственный API-роут, тот обращается к Т‑Банку методом Init и получает ссылку на страницу оплаты. Дальше пользователя уводят на securepay.tinkoff.ru, он вводит карту, а Т‑Банк параллельно двумя путями сообщает вам результат: редиректом обратно на SuccessURL/FailURL (это видит пользователь) и отдельным POST-запросом на NotificationURL - это видит только ваш сервер, и именно на этот запрос нужно ориентироваться при смене статуса заказа в базе, потому что редирект пользователь может закрыть или у него оборвётся связь.

Есть два варианта, как встроить оплату в лендинг:

Способ Что видит клиент Срок реализации Когда беру
Редирект на страницу Т‑Банка Уходит с сайта на securepay.tinkoff.ru, после оплаты возвращается 1-2 дня Для большинства лендингов - надёжнее и меньше багов с CSP
Встроенный виджет (checkout.js) Форма оплаты прямо на странице, в модалке или инлайне 3-5 дней Когда критично не терять пользователя со страницы конверсии

На практике для одностраничника с одной кнопкой «Купить» редирект окупается быстрее - меньше точек отказа, не нужно возиться с CSP и отдельным скриптом. Виджет ставлю, когда лендинг многошаговый и заказчик прямо просит не уводить с домена.

Что получить от Т‑Банка до старта интеграции

Без юрлица или ИП с расчётным счётом договор на эквайринг не оформить - это отдельная заявка в личном кабинете Т‑Банк Бизнес, рассмотрение обычно занимает от одного до трёх рабочих дней. После одобрения выдают два значения, без которых весь код ниже бесполезен: TerminalKey (публичный идентификатор терминала) и Password (секретный ключ для подписи запросов, его не светите на фронтенде и не коммитьте в репозиторий).

Важный момент, который часто упускают: тестовый терминал и боевой - это разные TerminalKey с разными Password, переключение между ними - это смена переменных окружения, а не кода. Ещё до первого запроса нужно в личном кабинете прописать NotificationURL, SuccessURL и FailURL - без белого списка урлов уведомления Т‑Банк просто не отправит, даже если код написан правильно.

Если планируете принимать оплату официально по 54-ФЗ, уточните у Т‑Банка подключение онлайн-кассы - для landing-страниц с разовыми услугами обычно берут готовую интеграцию с кассой от самого банка, чтобы не поднимать отдельный сервис фискализации.

Создаём платёж через API-роут Init в Next.js

Метод Init принимает POST на https://securepay.tinkoff.ru/v2/Init и обязательно требует подпись Token - SHA-256 хэш от всех переданных полей, отсортированных по алфавиту ключей, склеенных со значением Password. Сумма передаётся в копейках - забыть про это и отправить рубли вместо копеек это первая ошибка, с которой сталкивается почти каждый, кто делает интеграцию впервые.

// app/api/tbank/init/route.js
import crypto from 'crypto';

const TERMINAL_KEY = process.env.TBANK_TERMINAL_KEY;
const PASSWORD = process.env.TBANK_PASSWORD;

function buildToken(params) {
  const withPassword = { ...params, Password: PASSWORD };
  const sorted = Object.keys(withPassword)
    .sort()
    .map((key) => String(withPassword[key]))
    .join('');
  return crypto.createHash('sha256').update(sorted).digest('hex');
}

export async function POST(request) {
  const { orderId, amountRub, description } = await request.json();

  const payload = {
    TerminalKey: TERMINAL_KEY,
    Amount: Math.round(amountRub * 100),
    OrderId: orderId,
    Description: description,
    NotificationURL: 'https://example.ru/api/tbank/notification',
    SuccessURL: 'https://example.ru/payment/success',
    FailURL: 'https://example.ru/payment/fail',
  };

  const token = buildToken(payload);

  const res = await fetch('https://securepay.tinkoff.ru/v2/Init', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ ...payload, Token: token }),
  });

  const data = await res.json();

  if (!data.Success) {
    return Response.json({ error: data.Message }, { status: 400 });
  }

  return Response.json({ paymentUrl: data.PaymentURL });
}

OrderId должен быть уникальным на вашей стороне - сюда обычно кладут id заказа из своей базы, а не случайную строку, потому что именно по нему потом сверяют статус в вебхуке и при ручных проверках через GetState.

Бесплатный материал

🎁 Полезный скрипт в подарок

Подпишитесь на Telegram - пришлю готовый скрипт по этой теме.

Без спама. Отписка в 1 клик.

Принимаем и проверяем вебхук Notification от Т‑Банка

После каждой смены статуса платежа (NEW, AUTHORIZED, CONFIRMED, REJECTED и так далее) Т‑Банк шлёт POST на ваш NotificationURL и ждёт в ответ строку «OK» обычным текстом. Если сервер не ответил вовремя или ответил не тем, банк повторяет отправку по нарастающей - в моей практике это растягивалось на несколько часов подряд, что один раз завалило логи на проекте с не обработанной вовремя обработкой ошибок.

Доверять телу запроса нельзя без проверки - Token в самом уведомлении считается так же, как при Init: берутся все поля верхнего уровня кроме Token и вложенных объектов, сортируются по алфавиту, склеиваются со значением Password и хэшируются.

// app/api/tbank/notification/route.js
import crypto from 'crypto';

const PASSWORD = process.env.TBANK_PASSWORD;

function isValidToken(body) {
  const { Token, Receipt, DATA, ...rest } = body;
  const withPassword = { ...rest, Password: PASSWORD };
  const sorted = Object.keys(withPassword)
    .sort()
    .map((key) => String(withPassword[key]))
    .join('');
  const expected = crypto.createHash('sha256').update(sorted).digest('hex');
  return expected === Token;
}

export async function POST(request) {
  const body = await request.json();

  if (!isValidToken(body)) {
    return new Response('Bad token', { status: 400 });
  }

  if (body.Status === 'CONFIRMED') {
    // помечаем заказ body.OrderId оплаченным в своей базе
  }

  return new Response('OK');
}

Обработчик должен быть идемпотентным: один и тот же статус может прийти повторно, если банк не получил ваш «OK» с первого раза, поэтому перед обновлением заказа стоит проверять, не помечен ли он уже оплаченным. Готовый обработчик такого вебхука с проверкой подписи и логированием статусов уже собран и лежит в библиотеке готовых скриптов - экономит день на отладке подписи, если делаете интеграцию впервые.

Виджет оплаты на лендинге и редирект на страницу Т‑Банка

Для редирект-схемы фронтенд лендинга максимально простой: кнопка вызывает ваш /api/tbank/init, получает paymentUrl и переводит пользователя туда через window.location.href. Никакого дополнительного JS от Т‑Банка подключать не нужно, CSP трогать тоже не придётся.

Если делаете встроенный виджет, Т‑Банк даёт checkout.js, который рисует форму оплаты в iframe прямо на странице. Подключается скриптом в head, а дальше через объект TinkoffCheckout передаётся TerminalKey и PaymentId, полученный из ответа Init. Тут на практике всплывает нюанс, который не сразу очевиден: если у лендинга настроен строгий Content-Security-Policy, добавляйте frame-src и script-src для securepay.tinkoff.ru и widget.tinkoff.ru заранее, иначе виджет молча не откроется, а в консоли будет просто CSP violation без внятного намёка на причину.

Тестовый терминал, частые ошибки и переход в боевой режим

Список того, на чём реально спотыкаются при подключении оплаты на Next.js:

  • Token не совпадает - чаще всего забыли отсортировать ключи по алфавиту или включили в подпись вложенный объект Receipt, который туда не входит
  • Amount передан в рублях, а не в копейках - платёж создаётся, но на сумму в сто раз меньше
  • NotificationURL недоступен извне - типичная ситуация при локальной разработке, когда забыли прокинуть туннель ngrok или cloudflared и удивляются, почему статус в базе не обновляется
  • Тестовые карты не подключены - в личном кабинете тестового терминала есть отдельный список номеров карт для эмуляции успешной и отклонённой оплаты, реальные карты на тестовом терминале не пройдут
  • Забыли, что TerminalKey и Password для боевого терминала - это отдельная пара значений, активируется по отдельной заявке и может занять пару рабочих дней

Рыночная комиссия за эквайринг у большинства банков держится в районе 1,5-2,7% от суммы платежа в зависимости от оборота - это не мои расценки, а общий ориентир по рынку, точный процент вам согласует менеджер Т‑Банка под конкретный оборот.

Перед переключением на боевой терминал прогоняю на тестовом полный цикл минимум три раза: успешная оплата, отклонённая карта, повторный вебхук с тем же статусом - только после этого меняю переменные окружения на боевые значения и деплою.

Быстрый SEO-лендинг под продукт

Лендинг на Next.js

от 80 000 ₽

Подробнее →

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

Сколько по времени занимает интеграция Т‑Банка на Next.js лендинг?

При готовом API-роуте и уже одобренном терминале сама разработка занимает 1-2 дня для редирект-схемы и 3-5 дней для встроенного виджета. Отдельно закладывайте время на рассмотрение заявки в Т‑Банке - обычно один-три рабочих дня на тестовый терминал и ещё пару дней на активацию боевого.

Можно ли принимать оплату по СБП тем же способом?

Да, СБП включается на уровне терминала в личном кабинете Т‑Банка, а код Init и обработка Notification остаются теми же - банк сам показывает пользователю QR-код или ссылку на оплату через приложение банка на странице, куда вы его редиректите.

Нужна ли онлайн-касса для приёма оплаты через Т‑Банк?

Если продаёте товары или услуги физлицам за деньги, по 54-ФЗ фискализация обязательна. Т‑Банк предлагает готовую интеграцию с онлайн-кассой прямо в рамках эквайринга - через объект Receipt в запросе Init, чек формируется автоматически без отдельного сервиса.

Что делать, если вебхук не приходит на локальный сервер при тестировании?

NotificationURL должен быть доступен из интернета, localhost для этого не подходит. На время разработки поднимаю туннель через ngrok или cloudflared, прописываю выданный временный домен в личном кабинете как NotificationURL и уже после проверки переключаю на боевой адрес перед деплоем.

Есть задача?

Обсудим в мессенджере

Расскажите, что нужно сделать — отвечу в течение 4 часов в рабочее время. Первая консультация бесплатно.

Самозанятый Калинкин Н. А. · работаю с физлицами и юрлицами

Продолжая пользование настоящим сайтом Вы выражаете своё согласие на обработку Ваших персональных данных (файлов куки) с использованием Yandex.Metrika.
Понятно