Skip to main content
Содержание гайда

Содержание гайда

Время на изучение: 20 мин
#monobank#acquiring#vibecoding#ai_tools#payments
NEWНовичок20 мин

Как подключить эквайринг Monobank без программиста через вайбкодинг

Пошаговое руководство по подключению онлайн-оплат Monobank на сайт: комплаенс для ФЛП, создание терминала, верификация вебхуков ECDSA, надежный UX и тесты безопасности.

Опубликовано:
ДЛЯ АГЕНТАChatGPTClaude

Вайбкодинг кардинально изменил разработку цифровых продуктов. Сегодня, чтобы добавить на сайт прием платежей через Apple Pay, Google Pay или банковские карты, больше не нужно нанимать бэкенд-разработчика или самостоятельно разбираться в криптографических протоколах. Достаточно уметь четко сформулировать бизнес-логику своему AI-агенту (Codex, Antigravity, Cursor или Claude Code) и предоставить ему правильный контекст технической документации.

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

Совет

Видеоверсия руководства: Если вы предпочитаете наглядный формат, посмотрите подробное практическое видео на YouTube →, где весь процесс показан на живом экране от первого промпта до реального списания средств.

Примечание

Пакет агентских скилов: Для максимальной точности генерации кода скачайте официальный пакет знаний для вашего AI-ассистента:
Скачать полный архив monobank-acquiring.zip (38 KB) →


1. Архитектура Hosted Checkout и жизненный цикл платежа

Monobank Acquiring работает по схеме Hosted Checkout (оплата на защищенной странице банка). Это означает, что вашему сайту не требуется сложная и дорогая сертификация безопасности PCI DSS, поскольку платежные данные карт пользователь вводит непосредственно на защищенном домене банка.

Официальная документация Monobank для AI-инструментов доступна по адресу monobank.ua/api-docs/acquiring/dev/ai-tools/docs--ai-prompts →. Сохраните эту ссылку как канонический первоисточник технических требований банка.

1.1. Базовый платежный сценарий

terminal
[ Клиент на сайте ] │ ├─ 1. Нажимает «Оплатить» ▼ [ Ваш Сервер ] ──────────────► [ Monobank API: /invoice/create ] ▲ │ │ получает pageUrl, invoiceId │ └──────────────────────────────┘ │ ├─ 2. Редирект клиента на pageUrl (Hosted Checkout) ▼ [ Платежная страница Monobank ] ──► Оплата (Apple Pay / Google Pay / Карта) │ ├─ 3. Асинхронный POST вебхук с подписью x-sign ▼ [ Ваш Вебхук-эндпоинт ] ─────────► Проверка ECDSA SHA-256 → Статус "success" │ ├─ 4. Клиент возвращается на /payment-result ▼ [ Страница успеха ] ────────────► Отображение доступа / чека
  • 1. Создание счета (invoice/create): Клиент на вашем сайте выбирает товар или тариф. Серверный обработчик отправляет POST-запрос к Monobank API с суммой, назначением платежа и обратными адресами. Банк возвращает уникальный invoiceId и ссылку pageUrl.
  • 2. Редирект на платежную страницу: Клиент перенаправляется на pageUrl, где оплачивает заказ через Apple Pay, Google Pay, приложение Monobank или ввод данных банковской карты.
  • 3. Получение Webhook: После завершения оплаты Monobank автоматически отправляет POST-запрос на ваш заранее настроенный webHookUrl со статусом транзакции и криптографической цифровой подписью.
  • 4. Фиксация статуса (Terminal States): Ваш сервер обязан реагировать на два финальных статуса: success (платеж успешен, открываем доступ или отправляем заказ) и failure (ошибка или отклонение платежа банком).
Важно

Статус expired (истекло время действия ссылки) не генерирует вебхук. Если клиент закрыл платежную страницу и не оплатил, проверять состояние заказа необходимо через резервный опрос API (polling).


2. Юридические предпосылки и чек-лист комплаенса сайта для ФЛП

Даже если код оплаты написан безупречно, служба безопасности и финансового мониторинга Monobank не активирует боевой прием платежей, если сайт не соответствует требованиям законодательства Украины и правилам платежных систем Visa/Mastercard.

2.1. Банковские условия

  • Открытый счет ФЛП или юрлица в Monobank: Эквайринг подключается исключительно к предпринимательским счетам. На личную черную или белую карту физлица принимать коммерческие платежи по закону запрещено.
  • Подходящие КВЭДы: В регистрационных данных ФЛП должны быть указаны коды деятельности для интернет-торговли или услуг (например, 47.91 — Розничная торговля через интернет, 62.01/62.02 — Компьютерное программирование и консультации, 85.59 — Другие виды образования).

2.2. Обязательный чек-лист страниц перед подачей на модерацию

Перед тем как подать заявку на активацию интернет-терминала, убедитесь, что на вашем сайте есть следующие разделы (обычно размещаются в футере):

  • Публичная оферта (Договор публичной оферты): Описание предмета договора, момента заключения сделки, прав и обязанностей сторон.
  • Политика конфиденциальности (Privacy Policy): Четкое описание того, какие данные клиентов собираются и как они защищаются согласно закону «О защите персональных данных».
  • Условия возврата средств и доставки: Порядок возврата товара или средств в течение 14 дней по Закону Украины «О защите прав потребителей», а для цифровых услуг/подписок — правила отказа от услуги.
  • Полные реквизиты предпринимателя в футере: Наименование ФЛП или ООО, ИНН / ЕГРПОУ, юридический адрес, контактный номер телефона и рабочий e-mail службы поддержки.
  • Прозрачные цены и описание: Каждая кнопка оплаты должна иметь фиксированную цену в гривнах (UAH) и понятное описание того, за что именно платит клиент.
Внимание

Если на сайте нет оферты, реквизитов ФЛП или вместо цен стоят заглушки «по договоренности», служба безопасности Monobank отклонит регистрацию терминала на этапе проверки.


3. Создание веб-терминала в кабинете Monobank Бизнес

Для взаимодействия с платежным API вам необходим персональный ключ доступа — X-Token. Он генерируется бесплатно внутри личного кабинета предпринимателя.

3.1. Пошаговый алгоритм открытия кассы

  1. Авторизация: Перейдите на web.monobank.ua → и войдите с помощью QR-кода через приложение Monobank на смартфоне.
  2. Переход к кассе: В левом навигационном меню выберите раздел «Касса».
  3. Добавление инструмента: Нажмите кнопку «+ Добавить инструмент» и выберите вариант «Оплаты на сайте (собственная разработка)».
  4. Регистрация терминала: Введите название проекта (например, Мой сайт или Основной терминал) и подтвердите операцию кнопкой «Подключить».
  5. Генерация API-токена: Откройте созданный терминал, перейдите во вкладку «Интеграции / API Ключи», нажмите «Создать токен» и скопируйте сгенерированный ключ.

📍 Навигация в кабинете: web.monobank.ua → Касса → + Добавить инструмент → Оплаты на сайте (собственная разработка)

Создание платежного инструмента в кабинете Monobank Kasa ЗбільшитиСоздание платежного инструмента в кабинете Monobank KasaСоздание платежного инструмента в кабинете Monobank Kasa

📍 Получение API-ключа: Касса → Ваш терминал → Интеграция → Создать X-Token

Модальное окно создания и копирования токена доступа X-Token ЗбільшитиМодальное окно создания и копирования токена доступа X-TokenМодальное окно создания и копирования токена доступа X-Token

3.2. Железные правила безопасности токенов

  • Никакого хардкода в клиентском коде: X-Token дает прямой доступ к управлению вашими финансами и возвратами. Его категорически запрещено хранить в открытом JavaScript/HTML или публиковать в открытых репозиториях GitHub.
  • Использование переменных окружения: Храните токен исключительно в файле .env на сервере под именем MONOBANK_TOKEN или в разделе Secrets вашей хостинг-платформы (Vercel, Render, Railway, Replit, Lovable).
  • Тестовый токен для разработки: Для начальных экспериментов банк предоставляет отдельный тестовый токен на странице api.monobank.ua →, который позволяет симулировать транзакции без списания реальных средств.

4. Официальные AI-промпты Monobank для вайб-кодеров

Команда Monobank разработала набор официальных системных промптов для AI-агентов. Их главное преимущество — точное соответствие актуальным эндпоинтам банка.

Страница официальной документации Monobank для AI-инструментов ЗбільшитиСтраница официальной документации Monobank для AI-инструментовСтраница официальной документации Monobank для AI-инструментов
Внимание

Главная ловушка новичков — сумма в копейках: Monobank API принимает все суммы исключительно в минимальных единицах валюты (копейках). $100\text{ грн} = 10,000\text{ копеек}$. Если вы передадите amount: 100, клиент заплатит всего 1 гривну.

4.1. Базовый промпт создания платежа

Скопируйте этот промпт и отправьте его в чат вашего AI-ассистента:

markdown
Я хочу добавить на свой сайт возможность принимать платежи через Monobank. ЗАДАЧА: Напиши полный готовый код для создания платежа и перенаправления пользователя на оплату. ЧТО ДОЛЖНО ПРОИЗОЙТИ: 1. На моем сайте кнопка "Перейти к оплате". 2. Пользователь кликает → создается счет в Monobank. 3. Пользователь переходит на страницу оплаты Monobank. 4. После оплаты он возвращается на мой сайт (https://mysite.com/payment-result). 5. Я проверяю статус платежа и показываю результат. ТЕСТОВЫЕ ДАННЫЕ: - Сумма: 100 грн (10000 копеек) - Описание платежа: "Оплата заказа" - Куда возвращать пользователя: https://mysite.com/payment-result МОЙ ТОКЕН MONOBANK: Переменная окружения MONOBANK_TOKEN из файла .env ДОКУМЕНТАЦИЯ: - Создание платежа: https://monobank.ua/api-docs/acquiring/methods/ia/post--api--merchant--invoice--create - Проверка статуса: https://monobank.ua/api-docs/acquiring/methods/ia/get--api--merchant--invoice--status Напиши весь код для этого flow.

4.2. Промпт для настройки Webhook-обработчика

Без вебхука сервер не узнает о факте успешной оплаты, если клиент закроет браузер сразу после списания средств:

markdown
Мне нужно, чтобы мой сервер автоматически узнавал, когда пользователь оплатил заказ. ЗАДАЧА: Настрой автоматическое получение информации о платежах от Monobank (webhook). ЧТО ДОЛЖНО ПРОИЗОЙТИ: 1. Пользователь оплачивает на странице Monobank. 2. Monobank автоматически отправляет POST-запрос на мой сервер. 3. Мой сервер получает данные: invoiceId, статус (success/failure), сумму. 4. Сервер проверяет криптографическую подпись (ECDSA SHA-256 через заголовок x-sign). 5. Обновляет статус заказа в базе данных и логирует результат. ВАЖНО: - Обязательно валидируй заголовок x-sign с помощью публичного ключа банка. - Обрабатывай сырое тело запроса (raw body Buffer / raw string), а не распарсенный JSON. - Обрабатывай статусы: success, failure, processing, hold, expired. - Обеспечь идемпотентность: если один и тот же вебхук придет дважды, не дублируй выдачу товара. Напиши весь код для надежного webhook-обработчика.

5. Пакет скилов monobank-acquiring: прокачка агента

Если ограничиться только коротким промптом, AI-ассистент напишет базовый код (примерно 6.8 из 10): кнопка сработает, но код не будет содержать проверки криптографических подписей, защиты от подмены цены и обработки сетевых ошибок.

Чтобы получить продакшен-уровень (9.8–10 баллов), в корень проекта добавляется специализированный пакет скилов monobank-acquiring.

Аудит платежной интеграции: сравнение безопасности до и после использования скила ЗбільшитиАудит платежной интеграции: сравнение безопасности до и после использования скилаАудит платежной интеграции: сравнение безопасности до и после использования скила
bash
# Распаковка пакета скилов в проект curl -L -o monobank-acquiring.zip https://gotburnout.io/downloads/monobank-acquiring.zip unzip monobank-acquiring.zip -d monobank-acquiring/

5.1. Анатомия и структура пакета скилов

Файл скилаЧто содержит и за какие задачи отвечает
SKILL.mdЦентральный манифест: базовый флоу, авторизация X-Token, типы данных и обработка ошибок 400, 403, 429, 500.
quickstart.mdБыстрый старт: пошаговый туториал создания инвойса и фолбек-поллинга с готовыми curl-командами.
invoice.mdЖизненный цикл счетов: эндпоинты создания инвойса, проверки статуса, отмены и инвалидации ссылок.
webhook.mdКриптографическая защита: математически точная верификация ECDSA SHA-256 подписи из заголовка x-sign.
payment.mdПрямые платежи: списание по сохраненному токену карты, синхронные транзакции и 3DS-проверка.
wallet.mdТокенизация карт (Wallet): безопасное сохранение платежного средства клиента в хранилище банка для оплат в один клик.
fiscal.mdЧеки и пРО: описание структуры товарной корзины basketOrder, расчет налогов, скидок и экспорт чеков в PDF.
statement.mdВыписки и аналитика: получение реестра успешных операций за выбранный период с расчетом банковских комиссий.
merchant.mdДанные мерчанта: получение публичного ключа банка, управление субмерчантами и кассирами.
examples/Готовые серверы: рабочие примеры серверов на 6 языках (Node.js, Python, Go, PHP, C#, Java).

6. Практическая реализация: динамический платежный флоу

В каждом проекте своя структура: цифровые консультации с фиксированными тарифами, интернет-магазин с динамической корзиной или простой лендинг с кнопкой подписки.

Главная ошибка новичков — жестко «зашивать» сумму (например, 1000 грн) прямо в клиентский код кнопки или в тело POST-запроса из браузера. Этот подход создает критическую уязвимость безопасности.

6.1. Принцип безопасности: динамическая цена вместо хардкода

  • Никогда не доверяйте сумме из браузера: Если клиентский JavaScript отправляет на сервер { price: 1000 }, злоумышленник через DevTools или Postman может подменить это значение на { price: 1 } и приобрести товар за 1 гривну.

  • Сервер — единственный источник правды (Single Source of Truth): Фронтенд передает на бэкенд только идентификатор товара (productId), выбранный тариф (planId: "pro") или массив идентификаторов корзины (items: [{ id: "book_1", qty: 2 }]).

  • Автоматическая конвертация в копейки: Сервер извлекает актуальную стоимость из конфигурационного файла или базы данных и самостоятельно умножает ее на 100:

    $$\text{amount} = \text{Math.round}(\text{realPrice} \times 100)$$

6.2. Универсальный AI-промпт для адаптации под любой проект

Скопируйте этот промпт и отправьте его в чат вашего AI-агента (Codex, Antigravity, Cursor или Claude Code). Агент сам просканирует файлы вашего сайта, найдет существующие кнопки и цены и построит надежную интеграцию:

markdown
В корень нашего проекта добавлен официальный пакет знаний Monobank Acquiring в папку /monobank-acquiring. ЗАДАЧА: 1. Проанализируй архитектуру нашего проекта: найди, где у нас описаны товары, услуги, тарифы или кнопки оформления заказа/оплаты. 2. Создай или интегрируй защищенный серверный эндпоинт создания платежа Monobank: - Клиент отправляет ТОЛЬКО идентификатор товара/тарифа (например, tariffId или productId), но НЕ сумму. - Сервер находит реальную актуальную цену из нашего конфига или базы данных и переводит ее в копейки (цена * 100). - Токен берется безопасно из process.env.MONOBANK_TOKEN. - Делается POST-запрос к https://api.monobank.ua/api/merchant/invoice/create. - Клиенту возвращается ссылка pageUrl для перехода на чекаут банка. 3. Обнови наши существующие кнопки оплаты на фронтенде: - Добавь статус загрузки (индикатор Loader и блокировку кнопки от повторного клика). - При успешном ответе выполняй плавный редирект пользователя на полученную ссылку Monobank. - Добавь обработку ошибок с понятными уведомлениями. 4. Создай страницу результата оплаты (/payment-result), которая проверяет статус заказа и информирует пользователя об успехе. 5. Напиши юнит-тесты для проверки динамического расчета сумм и создания инвойса. Все технические требования, структуры запросов и коды валют бери из файлов в папке /monobank-acquiring (особенно SKILL.md и invoice.md).
Работа AI-агента в IDE с анализом структуры и созданием динамического эндпоинта ЗбільшитиРабота AI-агента в IDE с анализом структуры и созданием динамического эндпоинтаРабота AI-агента в IDE с анализом структуры и созданием динамического эндпоинта

6.3. Архитектурный шаблон бэкенда создания инвойса

typescript
// app/api/checkout/create-invoice/route.ts import { NextResponse } from "next/server"; const PRODUCTS_CATALOG: Record<string, { title: string; priceUah: number }> = { plan_starter: { title: "Тариф Starter", priceUah: 490 }, plan_pro: { title: "Тариф Pro", priceUah: 990 }, plan_vip: { title: "Тариф VIP", priceUah: 2490 }, }; export async function POST(req: Request) { try { const { productId } = await req.json(); // 1. Валидация: цена формируется исключительно на бэкенде const product = PRODUCTS_CATALOG[productId]; if (!product) { return NextResponse.json({ error: "Выбранный товар или тариф не найден" }, { status: 400 }); } const amountInKopecks = Math.round(product.priceUah * 100); const orderReference = `order_${productId}_${Date.now()}`; const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://mysite.com"; // 2. Запрос к Monobank API const response = await fetch("https://api.monobank.ua/api/merchant/invoice/create", { method: "POST", headers: { "X-Token": process.env.MONOBANK_TOKEN!, "Content-Type": "application/json", }, body: JSON.stringify({ amount: amountInKopecks, ccy: 980, // Гривна (ISO 4217) merchantPaymInfo: { reference: orderReference, destination: `Оплата: ${product.title}`, comment: `Заказ ${orderReference}`, }, redirectUrl: `${siteUrl}/payment-result?ref=${orderReference}`, webHookUrl: `${siteUrl}/api/payment/webhook`, validity: 3600, // Счет действует 1 час }), }); const data = await response.json(); if (!response.ok) { return NextResponse.json({ error: data.errText || "Ошибка банка при создании счета" }, { status: response.status }); } return NextResponse.json({ checkoutUrl: data.pageUrl, invoiceId: data.invoiceId }); } catch (error) { return NextResponse.json({ error: "Внутренняя ошибка инициализации платежа" }, { status: 500 }); } }

7. Безопасная обработка вебхуков и криптографическая подпись ECDSA

Самая ответственная часть любой финансовой интеграции — верификация уведомлений об оплате. Злоумышленник может отправить поддельный HTTP-запрос на ваш адрес /api/payment/webhook с фальшивым сообщением об успехе.

Чтобы предотвратить это, Monobank подписывает каждый вебхук с помощью асимметричного алгоритма ECDSA (кривая secp256r1 / SHA-256) и передает сигнатуру в HTTP-заголовке x-sign.

7.1. Почему JSON.stringify ломает верификацию подписи

Внимание

Критическая ловушка rawBody: Для проверки подписи ECDSA требуется строго оригинальный поток байтов, который отправил сервер Monobank. Если вы попытаетесь распарсить JSON и снова вызвать JSON.stringify(req.body), порядок ключей, пробелы или переносы строк изменятся. Это приведет к другому хешу SHA-256, и проверка подписи гарантированно вернет ошибку!

7.2. Реализация верификации вебхука

typescript
// app/api/payment/webhook/route.ts import { NextResponse } from "next/server"; import crypto from "crypto"; let cachedPubKey: string | null = null; async function getMonobankPubKey(token: string): Promise<string> { if (cachedPubKey) return cachedPubKey; const res = await fetch("https://api.monobank.ua/api/merchant/pubkey", { headers: { "X-Token": token }, next: { revalidate: 86400 }, // Кешируем публичный ключ на 24 часа }); const data = await res.json(); cachedPubKey = `-----BEGIN PUBLIC KEY-----\n${data.key}\n-----END PUBLIC KEY-----`; return cachedPubKey; } export async function POST(req: Request) { const signature = req.headers.get("x-sign"); if (!signature) { return new NextResponse("Missing x-sign header", { status: 400 }); } // 1. Получаем сырое неизмененное тело запроса в виде текста const rawBody = await req.text(); try { const pubKey = await getMonobankPubKey(process.env.MONOBANK_TOKEN!); // 2. Верифицируем подпись ECDSA SHA-256 const verifier = crypto.createVerify("SHA256"); verifier.update(rawBody); const isValid = verifier.verify(pubKey, Buffer.from(signature, "base64")); if (!isValid) { console.error("Вебхук отклонен: недействительная подпись x-sign"); return new NextResponse("Invalid signature", { status: 400 }); } // 3. Парсим данные только после успешной проверки криптографии const payload = JSON.parse(rawBody); const { invoiceId, status, amount, reference } = payload; if (status === "success") { // Активируем заказ в базе данных с проверкой дубликатов (идемпотентность) console.log(`Заказ ${reference} (${invoiceId}) оплачен на сумму ${amount / 100} грн`); } return new NextResponse("OK", { status: 200 }); } catch (error) { console.error("Ошибка обработки вебхука:", error); return new NextResponse("Internal verification error", { status: 500 }); } }

8. Локальное тестирование вебхуков (Localhost & Cloudflare Tunnels)

Частая проблема вайб-кодеров: при запуске сервера на http://localhost:3000 банк не может доставить вебхук, так как локальная машина не имеет публичного IP-адреса в интернете.

Monobank отправляет вебхуки исключительно на публичные адреса с действующим протоколом HTTPS. Чтобы протестировать полный цикл на своем компьютере, пробросьте безопасный туннель.

8.1. Быстрый запуск туннеля (без регистрации и бесплатно)

bash
# Мгновенный HTTPS-туннель на 3000 порт без установки утилит npx untun@latest tunnel --port 3000

После запуска вы получите временный публичный HTTPS-адрес: https://your-tunnel-name.trycloudflare.com

8.2. Настройка вебхука для локального теста

В коде создания инвойса во время разработки укажите полученный адрес:

typescript
webHookUrl: "https://your-tunnel-name.trycloudflare.com/api/payment/webhook"

Теперь при тестовой оплате банк отправит реальный вебхук прямо на ваш локальный сервер в терминале, и вы сможете убедиться, что подпись x-sign успешно проходит проверку.


9. UX страницы возврата и решение состояния гонки (Race Condition)

Когда клиент оплачивает счет через приложение Monobank или Apple Pay, банк возвращает его на адрес redirectUrl (/payment-result?ref=...) мгновенно.

Однако сетевой вебхук от сервера банка к вашему серверу может задержаться на 1–2 секунды из-за сетевых маршрутов. Если страница результата сразу проверит статус в базе данных, она рискует показать: «Заказ не оплачен», что вызовет панику у клиента (деньги с карты списаны, а сайт сообщает об отсутствии оплаты).

9.1. Инженерный паттерн решения гонки

  1. Начальное состояние загрузки: Страница открывается с нейтральным статусом: «Подтверждаем оплату в банке...» и анимированным индикатором.
  2. Короткий поллинг (Short Polling): Фронтенд делает до 5 быстрых запросов к собственному API каждые 1.5 секунды (/api/orders/check-status?ref=...), ожидая, пока вебхук изменит статус заказа в базе на success.
  3. Фолбек: Если за 8 секунд вебхук не дошел, клиент видит сообщение: «Платеж принят в обработку. Как только банк подтвердит транзакцию, доступ откроется автоматически».

9.2. Готовый React-компонент страницы результата

tsx
// app/payment-result/page.tsx "use client"; import { useEffect, useState } from "react"; import { useSearchParams, useRouter } from "next/navigation"; export default function PaymentResultPage() { const searchParams = useSearchParams(); const router = useRouter(); const ref = searchParams.get("ref"); const [status, setStatus] = useState<"checking" | "success" | "pending" | "failed">("checking"); useEffect(() => { if (!ref) { setStatus("failed"); return; } let attempts = 0; const maxAttempts = 5; const interval = setInterval(async () => { attempts++; try { const res = await fetch(`/api/orders/status?ref=${encodeURIComponent(ref)}`); const data = await res.json(); if (data.status === "success") { clearInterval(interval); setStatus("success"); } else if (attempts >= maxAttempts) { clearInterval(interval); setStatus("pending"); } } catch (err) { if (attempts >= maxAttempts) { clearInterval(interval); setStatus("pending"); } } }, 1500); return () => clearInterval(interval); }, [ref]); return ( <div className="max-w-md mx-auto my-16 p-8 rounded-2xl bg-neutral-900 border border-neutral-800 text-center text-white"> {status === "checking" && ( <div> <div className="w-12 h-12 border-4 border-amber-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" /> <h2 className="text-xl font-semibold mb-2">Подтверждаем оплату...</h2> <p className="text-sm text-neutral-400">Получаем статус транзакции от банка. Подождите несколько секунд.</p> </div> )} {status === "success" && ( <div> <div className="w-12 h-12 bg-emerald-500/20 text-emerald-400 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl font-bold">✓</div> <h2 className="text-xl font-semibold mb-2">Оплата успешна!</h2> <p className="text-sm text-neutral-400 mb-6">Ваш заказ #{ref} успешно подтвержден.</p> <button onClick={() => router.push("/dashboard")} className="px-6 py-2.5 rounded-xl bg-amber-500 hover:bg-amber-400 text-black font-semibold transition"> Перейти в кабинет </button> </div> )} {status === "pending" && ( <div> <div className="w-12 h-12 bg-amber-500/20 text-amber-400 rounded-full flex items-center justify-center mx-auto mb-4 text-2xl font-bold">⏳</div> <h2 className="text-xl font-semibold mb-2">Платеж в обработке</h2> <p className="text-sm text-neutral-400 mb-6">Средства зарезервированы. Подтверждение поступит в течение 1–2 минут.</p> <button onClick={() => router.push("/")} className="px-6 py-2.5 rounded-xl bg-neutral-800 hover:bg-neutral-700 text-white font-medium transition"> На главную </button> </div> )} </div> ); }

10. Расширенные возможности: встроенное пРО, холдирование и Wallet

Monobank Acquiring предоставляет полный спектр инструментов для сложных бизнес-моделей:

Финальная страница оплаты Monobank Hosted Checkout с Apple Pay и картами ЗбільшитиФинальная страница оплаты Monobank Hosted Checkout с Apple Pay и картамиФинальная страница оплаты Monobank Hosted Checkout с Apple Pay и картами

10.1. Программное РО: бесплатный встроенный Checkbox в Кассе Monobank

Для большинства украинских ФЛП 2-й и 3-й групп обязательная фискализация онлайн-продаж является юридическим требованием.

Главное преимущество Monobank для предпринимателей — бесплатная встроенная интеграция с сервисом пРО Checkbox:

  • Включение в один клик: В кабинете web.monobank.ua перейдите в настройки созданного терминала и активируйте переключатель «Фискализация через Checkbox». Monobank сам бесплатно создает кассу и подписывает чеки вашим КЭП.
  • Автоматическая фискализация: Если у вас стандартный каталог товаров с одинаковой ставкой, вам даже не нужно менять код создания платежа — чек автоматически формируется по полю destination в назначении платежа.
  • Расширенная фискализация через API: Если вы продаете товары с разными ставками НДС или подакцизные позиции с кодами УКТ ВЭД, передавайте массив basketOrder внутри объекта merchantPaymInfo:
json
"merchantPaymInfo": { "reference": "order_1001", "destination": "Оплата онлайн-курса", "customerEmails": ["client@example.com"], "basketOrder": [ { "name": "Доступ к курсу по вайбкодингу", "qty": 1, "sum": 99000, "code": "SKU-COURSE-01", "unit": "шт.", "total": 99000 } ] }

10.2. Двухэтапная оплата (Hold)

Если вы продаете физические товары, которые могут отсутствовать на складе, используйте режим холдирования:

  • Блокировка: Передайте paymentType: "hold" при создании инвойса. Средства замораживаются на карте покупателя на срок до 9 дней.
  • Списание (Finalize): После проверки наличия товара вызовите /api/merchant/invoice/finalize. Можно списать как полную сумму, так и меньшую (например, если одной позиции не оказалось).
  • Отмена: Если товара нет, вызовите /api/merchant/invoice/cancel — платеж отменяется без каких-либо банковских комиссий для покупателя.

10.3. Сохранение карт и подписки (Wallet)

Если вы запускаете SaaS-сервис с регулярной ежемесячной подпиской, передайте saveCardData: true при создании первого инвойса. После успешной оплаты в вебхуке вернется walletId. Используйте этот токен для последующих автоматических списаний без повторного ввода данных карты клиентом.


11. Матрица безопасности и автоматические тесты (Vitest / Jest)

Оплата — это зона максимальной финансовой ответственности. Ошибка в обычной кнопке вызывает раздражение, но ошибка в платежном шлюзе приводит к прямым материальным убыткам или блокировке кассы банком.

Перед переходом в боевой режим запустите тестовую матрицу безопасности:

Тест безопасностиЧто именно проверяетсяОжидаемое поведение системы
1. Защита от изменения цены (Price Tampering)Клиент отправляет productId: "vip", но пытается подсунуть в запрос amount: 100 (1 грн)Сервер игнорирует поле amount от клиента, берет реальную цену из конфига (2490 грн = 249 000 коп). Если productId подделан — возвращает HTTP 400.
2. Блокировка без подписиНа эндпоинт /api/payment/webhook поступает POST-запрос без заголовка x-signЗапрос немедленно блокируется с ответом HTTP 400 Bad Request. Никакой обработки заказа не происходит.
3. Блокировка фальшивых подписейЗлоумышленник отправил фейковую подпись в x-sign со статусом status: "success"Проверка crypto.verify(SHA256, ...) возвращает false. Сервер возвращает HTTP 400/401, доступ или товар не выдаются.
4. Идемпотентность вебхука (Duplicate Delivery)Monobank отправил одинаковый вебхук success дважды или трижды подряд из-за сетевого лагаДоступ или товар активируются только один раз. Повторный вебхук не вызывает дублирования выдачи, возвращая банку HTTP 200 OK.
5. Устойчивость к состоянию гонки (Race Condition)Вебхук от банка пришел раньше, чем завершился начальный HTTP-запрос создания инвойса в базеОбработчик вебхука устойчив к отсутствию записи в БД (использует UPSERT или создает заказ на лету).
6. Соблюдение рейт-лимитов (Rate Limit Buffer)При сбое вебхука запускается поллинг статуса /api/merchant/invoice/statusЗапросы выполняются с паузой не менее 15 секунд, что защищает терминал от блокировки HTTP 429 Too Many Requests.

11.1. Готовый набор автоматических тестов для вашего проекта

Попросите своего AI-агента добавить следующий тестовый набор в проект (monobank-acquiring.test.ts):

typescript
import { describe, it, expect } from "vitest"; import crypto from "crypto"; const { publicKey, privateKey } = crypto.generateKeyPairSync("ec", { namedCurve: "prime256v1", publicKeyEncoding: { type: "spki", format: "pem" }, privateKeyEncoding: { type: "pkcs8", format: "pem" }, }); describe("Monobank Acquiring Security Tests", () => { it("Тест 1: Не разрешает клиенту манипулировать ценой", () => { const CATALOG: Record<string, { priceUah: number }> = { plan_pro: { priceUah: 990 } }; const clientPayload = { productId: "plan_pro", amount: 100 }; // Попытка подсунуть 1 грн const safeAmount = Math.round(CATALOG[clientPayload.productId].priceUah * 100); expect(safeAmount).toBe(99000); // 990.00 грн, а не 1.00 грн }); it("Тест 2: Отклоняет вебхук без обязательного заголовка x-sign", () => { const headers: Record<string, string> = {}; const hasSignature = Boolean(headers["x-sign"]); expect(hasSignature).toBe(false); }); it("Тест 3: Успешно верифицирует валидную цифровую подпись банка", () => { const rawPayload = JSON.stringify({ invoiceId: "inv_123", status: "success", amount: 99000 }); const signer = crypto.createSign("SHA256"); signer.update(rawPayload); const validSignatureBase64 = signer.sign(privateKey, "base64"); const verifier = crypto.createVerify("SHA256"); verifier.update(rawPayload); const isValid = verifier.verify(publicKey, Buffer.from(validSignatureBase64, "base64")); expect(isValid).toBe(true); }); it("Тест 4: Блокирует поддельную подпись или модифицированное тело запроса", () => { const originalPayload = JSON.stringify({ invoiceId: "inv_123", status: "success", amount: 99000 }); const tamperedPayload = JSON.stringify({ invoiceId: "inv_123", status: "success", amount: 1000 }); const signer = crypto.createSign("SHA256"); signer.update(originalPayload); const signature = signer.sign(privateKey, "base64"); const verifier = crypto.createVerify("SHA256"); verifier.update(tamperedPayload); const isValid = verifier.verify(publicKey, Buffer.from(signature, "base64")); expect(isValid).toBe(false); }); it("Тест 5: Идемпотентность — повторный вебхук не выдает товар дважды", () => { const processedOrders = new Set<string>(); function handleOrder(invoiceId: string): { processed: boolean } { if (processedOrders.has(invoiceId)) { return { processed: false }; // Дубль отклонен } processedOrders.add(invoiceId); return { processed: true }; } expect(handleOrder("inv_001").processed).toBe(true); // Первый вебхук expect(handleOrder("inv_001").processed).toBe(false); // Повторный приход дубликата expect(processedOrders.size).toBe(1); }); });

12. Финальный инженерный чек-лист перед запуском

Проверьте свой платежный модуль по этим 10 пунктам перед переключением на боевые платежи:

  • Комплаенс сайта: В футере добавлены ссылки на Публичную оферту, Политику конфиденциальности, Условия возврата и полные реквизиты ФЛП с ИНН.
  • Суммы в копейках: Все значения amount умножены на 100 ($1\text{ грн} = 100\text{ коп}$) через Math.round.
  • Безопасность ключа: Токен вынесен в .env под именем MONOBANK_TOKEN и добавлен в .gitignore.
  • Цены из бэкенда (SSOT): Клиент передает только идентификатор товара, сумма берется исключительно из базы или конфига.
  • Обработка rawBody: Вебхук верифицирует оригинальный текстовый или бинарный буфер (req.text() или req.rawBody), избегая повторной сериализации через JSON.stringify.
  • Криптографическая защита: Вебхук верифицирует подпись x-sign через публичный ключ банка алгоритмом SHA256 ECDSA.
  • Публичный URL для вебхука: Сервер доступен извне через действительный SSL-сертификат HTTPS (для локальных тестов подходит Cloudflare Tunnel или ngrok).
  • Идемпотентность: Повторный приход одного и того же вебхука не вызывает повторную выдачу товара или подписки.
  • UX страницы результата: На /payment-result настроен лоадер ожидания и короткий поллинг статуса, чтобы защитить пользователя от состояния гонки.
  • Реальный платеж на 1–5 грн: Проведена успешная тестовая транзакция реальной картой, проверено списание средств и отображение в кабинете банка.

Полезные ресурсы и файлы для скачивания

Этот гайд полностью бесплатный. Если он сэкономил вам вечер — вы можете поддержать развитие проекта.
Поддержать автора