Используйте ngrok, чтобы получать webhook-уведомления ApiPay прямо на localhost. Видите запросы в реальном времени — без деплоя.
При разработке у вас нет публичного URL — ApiPay не может отправить webhook на localhost:3000. ngrok создаёт безопасный HTTPS-тоннель к вашему компьютеру и даёт публичный URL, доступный из интернета.
А встроенный Inspect UI (localhost:4040) показывает все входящие запросы — заголовки, тело, статус. Это идеальный инструмент для отладки вебхуков, даже если у вас ещё нет сервера.
Тестируйте в песочнице и на отдельном API-ключе. Через туннель проходят настоящие уведомления об оплатах, а Inspect UI показывает их тело целиком — рабочий ключ на туннель переключать не нужно.
Через Homebrew (macOS) или скачайте с сайта:
Зарегистрируйтесь на ngrok.com (бесплатно) и добавьте токен:
Укажите порт, на котором работает ваш локальный сервер (например, 3000):
Скопируйте HTTPS URL — это ваш публичный адрес. Он будет работать, пока ngrok запущен.
ngrok http --url=ваш-домен.ngrok-free.dev 3000 (один бесплатный домен в аккаунте).
Перейдите в ApiPay.kz → Настройки → Подключение и вставьте ngrok URL как Webhook URL:
Нажмите «Сохранить».
422 webhook_url_tunnel_forbidden — там нужен постоянный HTTPS-адрес на вашем домене.
X-Webhook-Signature считается по секрету, а не по ключу доступа. Если Webhook URL указан при создании API-ключа, секрет выдаётся сразу вместе с ключом; для уже созданного ключа действие «Создать секретный ключ подписи» появляется только после сохранения Webhook URL. Секрет показывается один раз — сохраните его сразу. Смена API-ключа секрет не меняет.
В настройках API ключа нажмите кнопку «Тест webhook». ApiPay отправит тестовое событие webhook.test на ваш ngrok URL.
Перейдите в браузере на localhost:4040. Это встроенная панель ngrok — показывает все входящие запросы:
X-Webhook-Signature)GET /invoices/{id}. Сохранение постоянного адреса и успешный «Тест webhook» возвращают отправку.
| Событие | Когда |
|---|---|
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. Обработчик должен спокойно игнорировать незнакомые события.
Каждый запрос содержит заголовок X-Webhook-Signature в формате sha256=<hex> — это HMAC-SHA256 от сырого тела запроса. При проверке стройте ожидаемое значение с тем же префиксом ('sha256=' + hex) и сравнивайте constant-time. Секрет вы получаете при создании webhook в настройках.
Если ваш сервер ответил 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с).
Скопируйте и запустите — принимает вебхуки, проверяет подпись, логирует в консоль
node server.js — затем в другом терминале ngrok http 3000. Вебхуки будут приходить и логироваться в консоль.
ngrok http --url=ваш-домен.ngrok-free.dev 3000. Тогда URL не будет меняться и не придётся обновлять webhook URL в ApiPay каждый раз.