Тестируйте вебхуки локально — без сервера, за 3 минуты

Используйте ngrok, чтобы получать webhook-уведомления ApiPay прямо на localhost. Видите запросы в реальном времени — без деплоя.

Зачем это нужно

При разработке у вас нет публичного URL — ApiPay не может отправить webhook на localhost:3000. ngrok создаёт безопасный HTTPS-тоннель к вашему компьютеру и даёт публичный URL, доступный из интернета.

А встроенный Inspect UI (localhost:4040) показывает все входящие запросы — заголовки, тело, статус. Это идеальный инструмент для отладки вебхуков, даже если у вас ещё нет сервера.

Тестируйте в песочнице и на отдельном API-ключе. Через туннель проходят настоящие уведомления об оплатах, а Inspect UI показывает их тело целиком — рабочий ключ на туннель переключать не нужно.

5 шагов — и вебхуки приходят на localhost

1

Установите ngrok

Через Homebrew (macOS) или скачайте с сайта:

# macOS $ brew install ngrok # Или скачайте: https://ngrok.com/download

Зарегистрируйтесь на ngrok.com (бесплатно) и добавьте токен:

$ ngrok config add-authtoken ВАШ_ТОКЕН
2

Запустите тоннель

Укажите порт, на котором работает ваш локальный сервер (например, 3000):

$ ngrok http 3000 Session Status online Forwarding https://a1b2c3d4.ngrok-free.dev -> http://localhost:3000

Скопируйте HTTPS URL — это ваш публичный адрес. Он будет работать, пока ngrok запущен.

Совет: URL меняется при каждом перезапуске ngrok. Для постоянного домена используйте ngrok http --url=ваш-домен.ngrok-free.dev 3000 (один бесплатный домен в аккаунте).
3

Вставьте URL в ApiPay

Перейдите в ApiPay.kz → Настройки → Подключение и вставьте ngrok URL как Webhook URL:

https://a1b2c3d4.ngrok-free.dev/webhook

Нажмите «Сохранить».

Туннель — для песочницы. В тестовом режиме такой адрес принимается. В рабочем режиме организация, ещё не прошедшая проверку анкеты о бизнесе, получит 422 webhook_url_tunnel_forbidden — там нужен постоянный HTTPS-адрес на вашем домене.
Секрет подписи — не API-ключ. Подпись X-Webhook-Signature считается по секрету, а не по ключу доступа. Если Webhook URL указан при создании API-ключа, секрет выдаётся сразу вместе с ключом; для уже созданного ключа действие «Создать секретный ключ подписи» появляется только после сохранения Webhook URL. Секрет показывается один раз — сохраните его сразу. Смена API-ключа секрет не меняет.
4

Отправьте тестовый вебхук

В настройках API ключа нажмите кнопку «Тест webhook». ApiPay отправит тестовое событие webhook.test на ваш ngrok URL.

Готово! Если ваш сервер запущен — он получит POST-запрос. Если нет — не страшно, переходите к шагу 5.
5

Откройте Inspect UI — видите всё

Перейдите в браузере на localhost:4040. Это встроенная панель ngrok — показывает все входящие запросы:

  • Заголовки (включая X-Webhook-Signature)
  • Тело запроса (JSON)
  • HTTP-статус ответа
  • Время ответа
Даже без сервера — Inspect UI покажет входящие запросы. ngrok вернёт 502 (потому что localhost не отвечает), но вы увидите, что именно отправил ApiPay. Это отличный способ понять формат вебхука до написания кода.
Закончив тест — верните постоянный адрес. Пока в поле стоит выключенный туннель, доставки на этот API-ключ копят неудачи: отправка вебхуков по ключу сначала приостанавливается, а после длинной череды отказов прекращается совсем, и события за это время не досылаются — состояние счетов после этого сверяют через GET /invoices/{id}. Сохранение постоянного адреса и успешный «Тест webhook» возвращают отправку.

Webhook API — справочник

События

Событие Когда
invoice.status_changed Статус счёта изменился (pending → paid, cancelled, expired)
invoice.refunded Создан возврат по оплаченному счёту
invoice.qr_scanned Покупатель отсканировал QR и открыл экран оплаты. Статус счёта остаётся pending — оплатой не считается
receipt.issued Фискальный чек Kaspi OFD выбит
receipt.failed Фискальный чек Kaspi OFD выбить не удалось
subscription.payment_succeeded Успешный платёж по подписке
subscription.payment_failed Неуспешный платёж по подписке
subscription.grace_period_started Начался grace-период подписки
subscription.expired Подписка истекла после grace-периода
webhook.test Тестовое событие (кнопка «Тест webhook»)

Это не полный перечень: помимо перечисленного есть события возвратов по QR, обработки каталога и жизненного цикла подписки. Полный список событий и полей payload — в документации API. Обработчик должен спокойно игнорировать незнакомые события.

Формат запроса

POST /webhook HTTP/1.1 Content-Type: application/json X-Webhook-Signature: sha256=<HMAC-SHA256 подпись тела в hex> { "event": "invoice.status_changed", "invoice": { "id": 42, "external_order_id": "order_abc", "status": "paid", "amount": "15000.00", "paid_at": "2026-01-15T10:30:00Z" }, "source": "my-api-key" }

Верификация подписи (HMAC-SHA256)

Каждый запрос содержит заголовок X-Webhook-Signature в формате sha256=<hex> — это HMAC-SHA256 от сырого тела запроса. При проверке стройте ожидаемое значение с тем же префиксом ('sha256=' + hex) и сравнивайте constant-time. Секрет вы получаете при создании webhook в настройках.

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

Retry-политика

Если ваш сервер ответил 5xx, 429 или не ответил вовсе (таймаут 5 секунд, сетевая ошибка), ApiPay повторит доставку: до 11 попыток (первая + 10 повторов) с нарастающей задержкой — 10с, 30с, 1м, 1.5м, 2м, 5м, 10м, 15м, 30м, 1ч, около 2 часов.

Остальные 4xx — в том числе 401 при несошедшейся подписи и 404 от закрытого туннеля — не повторяются: попытка сразу фиксируется как завершённая, и состояние счёта нужно сверять через GET /invoices/{id}. В песочнице invoice-вебхуки доставляются за 3 попытки (5с, 15с).

Минимальный сервер для вебхуков

Скопируйте и запустите — принимает вебхуки, проверяет подпись, логирует в консоль

server.js — Node.js (без зависимостей)
const http = require('http') const crypto = require('crypto') const PORT = 3000 const WEBHOOK_SECRET = 'ваш_секрет_из_настроек' // ApiPay → Настройки → Подключение const server = http.createServer((req, res) => { if (req.method === 'POST' && req.url === '/webhook') { let body = '' req.on('data', chunk => { body += chunk }) req.on('end', () => { // Проверка подписи. Заголовок приходит как 'sha256=' — // ожидаемое значение строим с тем же префиксом и сравниваем constant-time. const signature = req.headers['x-webhook-signature'] || '' const expected = 'sha256=' + crypto .createHmac('sha256', WEBHOOK_SECRET) .update(body) .digest('hex') const sigBuf = Buffer.from(signature) const expBuf = Buffer.from(expected) if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) { console.log('!! Подпись не совпадает') res.writeHead(401) return res.end('Invalid signature') } // Обработка события const data = JSON.parse(body) console.log('\n--- Webhook получен ---') console.log('Событие:', data.event) console.log('Данные:', JSON.stringify(data, null, 2)) console.log('---\n') res.writeHead(200, { 'Content-Type': 'application/json' }) res.end(JSON.stringify({ status: 'ok' })) }) } else { res.writeHead(404) res.end('Not found') } }) server.listen(PORT, () => { console.log(`Webhook сервер запущен: http://localhost:${PORT}/webhook`) console.log('Запустите ngrok: ngrok http ' + PORT) })
Запуск: node server.js — затем в другом терминале ngrok http 3000. Вебхуки будут приходить и логироваться в консоль.

Часто задаваемые вопросы

ngrok бесплатный?
Да. Для тестирования вебхуков бесплатного плана достаточно: он даёт постоянный dev-домен и месячный лимит запросов с запасом. Актуальные лимиты и цены — на ngrok.com/pricing: это условия стороннего сервиса, они меняются.
Нужна ли кредитная карта?
Для HTTP/HTTPS-тоннелей карта обычно не требуется, для TCP-эндпоинтов — требуется. Условия задаёт ngrok, проверяйте их при регистрации.
Что показывает localhost:4040?
Это ngrok Inspect UI — веб-панель, где видны все HTTP-запросы, прошедшие через тоннель. Для каждого запроса показаны: метод, URL, заголовки, тело запроса, HTTP-статус ответа и время. Можно повторить (replay) любой запрос. Работает даже если ваш сервер не запущен — вы увидите 502-ответы, но тело запроса будет видно.
URL меняется при перезапуске — это проблема?
На бесплатном плане ngrok даёт один постоянный dev-домен. Используйте его: ngrok http --url=ваш-домен.ngrok-free.dev 3000. Тогда URL не будет меняться и не придётся обновлять webhook URL в ApiPay каждый раз.
Есть ли альтернативы ngrok?
Да: LocalXpose, Cloudflare Tunnel (бесплатно), Loophole. Принцип тот же — создают тоннель к localhost. ngrok самый популярный и имеет удобный Inspect UI.
Это безопасно? Мой localhost доступен из интернета?
ngrok создаёт тоннель только к указанному порту. Остальные порты и файлы остаются недоступными. Тоннель работает только пока ngrok запущен — после закрытия URL перестаёт работать. Для production используйте настоящий сервер с HTTPS.

Связанные страницы

Интеграция Kaspi PayПошаговое подключение к сайту Интеграция с LovableПлатежи в no-code приложении Интеграция с n8nАвтоматизация платежей без кода Документация REST APIПолный справочник эндпоинтов