Kaspi Pay webhook: что это и как настроить через AiPay
Разбираем, что такое webhook Kaspi Pay, почему его нет из коробки и как добавить event-driven уведомления об оплате через AiPay за один час.
Вы создали Kaspi-счёт. Клиент заплатил. Ваш код об этом не знает
Представьте типичную ситуацию: интернет-магазин на Казахстан, оплата через Kaspi. Клиент переводит деньги — и дальше один из трёх сценариев:
- Менеджер вручную смотрит выписку — раз в 5–10 минут, в рабочее время, по будням.
- Скрипт делает polling — бьёт в API каждые 30 секунд, 99% запросов впустую, сервер молотит вхолостую.
- Клиент пишет «я оплатил» — и вы верите на слово, пока не проверите.
Все три способа ломаются ночью, в выходные и при любой нагрузке выше «несколько заказов в день». Правильное решение — webhook: ваш сервер не опрашивает систему, а система сама стучит к вам, когда что-то произошло. Именно это мы сейчас и настроим.
Что такое webhook и чем он отличается от polling
Представьте две модели отслеживания доставки пиццы.
Polling — вы каждые 5 минут открываете приложение и проверяете статус. Большинство раз — «в пути», ничего не изменилось. Вы тратите время и внимание впустую.
Webhook — приложение само присылает вам пуш, когда курьер вышел, когда он рядом, когда позвонил в дверь. Вы ничего не делаете до момента, когда нужно открыть дверь.
В разработке всё то же самое. Polling — это когда ваш код периодически спрашивает «ну что, платёж прошёл?». Webhook — это когда платёжная система сама отправляет HTTP POST-запрос на ваш эндпоинт в момент события.
Polling: ваш сервер → API (×N раз) → «нет данных» / «нет данных» / «нет данных» / «оплачено»
Webhook: платёжная система → ваш сервер (один раз, в нужный момент)
Webhook — это event-driven архитектура: код выполняется только тогда, когда что-то реально произошло. Это надёжнее, дешевле по ресурсам и проще в обслуживании.
Почему Kaspi Pay не отправляет вебхуки сам
Kaspi — крупнейшая финтех-платформа Казахстана: 14,7 миллиона активных пользователей в месяц, 737 000 торговцев. Для офлайн-бизнеса Kaspi работает отлично: кассир видит push в приложении, транзакция отражается в личном кабинете.
Но Kaspi Pay не предоставляет публичного webhook API для внешних разработчиков. Это означает, что ваш сервер не может напрямую подписаться на событие «счёт #12345 оплачен». Причин несколько:
- Kaspi изначально строился вокруг B2C-потока, где уведомление идёт пользователю в приложение, а не на внешний сервер разработчика.
- Публичный webhook API требует инфраструктуры для управления подписками, повторных попыток доставки, мониторинга — это отдельный продукт.
- На момент написания этой статьи официальная интеграция для независимых разработчиков через webhook-события публично недоступна.
Результат: разработчики вынуждены использовать обходные пути — парсинг email/SMS, неофициальные методы, постоянный polling. Все они нестабильны в продакшене.
AiPay — это middleware, который закрывает именно этот пробел. Он работает с Kaspi через официальные партнёрские механизмы и добавляет полноценный webhook-слой поверх.
Как работает AiPay webhook: архитектура за 2 минуты
Вот как выглядит полный поток от инициации оплаты до выполнения заказа:
Ваш сервис AiPay Kaspi Pay Клиент
─────────────────────────────────────────────────────────────────────
POST /invoices → Создаёт счёт → Push-уведомление → [видит в приложении]
[нажимает «Оплатить»]
Получает статус ← Подтверждение ← [платёж прошёл]
POST /webhook ← Отправляет ←
[проверяет HMAC]
[обновляет заказ]
[выдаёт товар]
Ключевые компоненты:
- REST API — вы создаёте счёт одним POST-запросом, получаете
invoice_id. - Webhook delivery — AiPay сам следит за статусом счёта и стреляет HTTP POST на ваш URL при любом изменении.
- HMAC-SHA256 подпись — каждый запрос подписан, вы можете проверить подлинность.
- Retry-логика — если ваш сервер временно недоступен, AiPay повторит доставку.
- Polling как fallback — если webhook по какой-то причине не дошёл, можно запросить статус через
GET /invoices/{id}.
Пошаговый поток AiPay webhook
- Клиент инициирует оплату — нажимает кнопку «Оплатить» в вашем интерфейсе (сайт, бот, приложение).
- Ваш бэкенд создаёт счёт — POST-запрос к AiPay с суммой и номером телефона клиента.
- AiPay создаёт счёт в Kaspi — клиент получает push-уведомление в приложении Kaspi с запросом на оплату.
- Клиент подтверждает платёж — одно нажатие в приложении Kaspi.
- AiPay получает подтверждение — фиксирует изменение статуса счёта.
- AiPay отправляет webhook — HTTP POST на ваш эндпоинт с данными о платеже и HMAC-подписью.
- Ваш сервер обрабатывает событие — проверяет подпись, проверяет идемпотентность, выполняет бизнес-логику.
- Заказ выполнен — товар выдан, статус обновлён, клиент доволен.
Весь цикл (шаги 3–8) занимает 20–60 секунд. Без ручного участия, в любое время суток.
Пример кода: создание счёта через AiPay API
Для начала нужно создать счёт. Это один POST-запрос.
cURL:
curl -X POST https://api.aipay.kz/v1/invoices \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "77001234567",
"amount": 12900,
"description": "Заказ #4521 — доставка",
"external_id": "order_4521"
}'
Ответ:
{
"invoice_id": "inv_k7x9m2p",
"status": "pending",
"amount": 12900,
"phone": "77001234567",
"created_at": "2026-03-27T10:15:00Z",
"expires_at": "2026-03-27T10:30:00Z"
}
Python (httpx):
import httpx
AIPAY_API_KEY = "your_api_key_here"
AIPAY_BASE_URL = "https://api.aipay.kz/v1"
async def create_kaspi_invoice(
phone: str,
amount: int,
external_id: str,
description: str = "",
) -> dict:
"""
Создаёт счёт в Kaspi Pay через AiPay API.
:param phone: номер телефона клиента в формате 77XXXXXXXXX
:param amount: сумма в тенге (целое число)
:param external_id: ваш внутренний ID заказа для матчинга webhook
:param description: описание платежа (видит клиент в Kaspi)
:return: dict с invoice_id, status и прочими полями
"""
async with httpx.AsyncClient() as client:
response = await client.post(
f"{AIPAY_BASE_URL}/invoices",
headers={
"Authorization": f"Bearer {AIPAY_API_KEY}",
"Content-Type": "application/json",
},
json={
"phone": phone,
"amount": amount,
"description": description,
"external_id": external_id,
},
)
response.raise_for_status()
return response.json()
Сохраните invoice_id из ответа — он понадобится, если нужно сделать polling как fallback. Поле external_id — это ваш собственный ID заказа, который AiPay вернёт обратно в webhook-событии.
Пример кода: обработчик webhook с проверкой HMAC
Это самая важная часть. Получить POST-запрос недостаточно — нужно убедиться, что он пришёл именно от AiPay.
Python (Flask):
import hmac
import hashlib
import json
from flask import Flask, request, jsonify, abort
app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret_here"
# Множество обработанных invoice_id для идемпотентности
# В продакшене используйте Redis или таблицу в БД
processed_invoices: set[str] = set()
def verify_aipay_signature(body: bytes, signature: str) -> bool:
"""Проверяет HMAC-SHA256 подпись от AiPay."""
expected = hmac.new(
WEBHOOK_SECRET.encode("utf-8"),
body,
hashlib.sha256,
).hexdigest()
# compare_digest защищает от timing-атак
return hmac.compare_digest(expected, signature)
@app.route("/webhook/aipay", methods=["POST"])
def handle_aipay_webhook():
# 1. Читаем сырое тело ДО парсинга JSON — нужно для проверки подписи
body = request.get_data()
signature = request.headers.get("X-Signature", "")
# 2. Проверяем подпись — если неверна, отклоняем
if not verify_aipay_signature(body, signature):
abort(403)
payload = json.loads(body)
invoice_id = payload.get("invoice_id")
status = payload.get("status")
# 3. Идемпотентность: не обрабатываем одно событие дважды
if invoice_id in processed_invoices:
return jsonify({"status": "already_processed"}), 200
# 4. Обрабатываем только успешные платежи
if status == "paid":
external_id = payload.get("external_id")
amount = payload.get("amount")
timestamp = payload.get("timestamp")
# Ваша бизнес-логика здесь:
# - обновить статус заказа в БД
# - отправить уведомление клиенту
# - запустить доставку / выдать доступ
fulfill_order(external_id, amount)
processed_invoices.add(invoice_id)
elif status == "expired":
# Счёт истёк — уведомить клиента, предложить повторить оплату
handle_expired_invoice(payload.get("external_id"))
elif status == "error":
# Что-то пошло не так на стороне Kaspi
handle_payment_error(payload.get("external_id"))
# 5. Всегда возвращаем 200 — иначе AiPay будет повторять запрос
return jsonify({"status": "ok"}), 200
def fulfill_order(external_id: str, amount: int):
"""Ваша логика выполнения заказа после успешной оплаты."""
print(f"Заказ {external_id} оплачен на сумму {amount} ₸")
# обновить БД, отправить уведомление, и т.д.
def handle_expired_invoice(external_id: str):
"""Обработка истёкшего счёта."""
print(f"Счёт для заказа {external_id} истёк")
def handle_payment_error(external_id: str):
"""Обработка ошибки платежа."""
print(f"Ошибка платежа для заказа {external_id}")
if __name__ == "__main__":
app.run(port=8000)
Полную документацию API смотрите на странице для разработчиков.
Безопасность: почему проверка HMAC обязательна
Без проверки подписи ваш /webhook/aipay эндпоинт — открытая дверь. Любой, кто знает его URL, может отправить поддельный POST-запрос с "status": "paid" и получить товар бесплатно.
Как работает HMAC-SHA256 защита:
- AiPay знает ваш
WEBHOOK_SECRET(он генерируется при регистрации и хранится только у вас). - При каждом webhook-запросе AiPay вычисляет
HMAC-SHA256(тело_запроса, WEBHOOK_SECRET)и кладёт результат в заголовокX-Signature. - Ваш сервер делает то же самое независимо и сравнивает результаты.
- Если подписи совпадают — запрос точно от AiPay и тело не было изменено в пути.
Три правила, которые нельзя нарушать:
- Всегда проверяйте подпись — до любой бизнес-логики.
- Используйте
hmac.compare_digest(или аналог в вашем языке) — обычное сравнение строк уязвимо к timing-атаке. - Вычисляйте HMAC от сырого тела — не от распарсенного JSON. JSON-парсеры могут изменить порядок ключей, и подпись не совпадёт.
Обработка граничных случаев
Повторные попытки (retries)
Если ваш сервер вернул не 200 (или не ответил в течение таймаута), AiPay повторит доставку через некоторое время. Это защита от кратковременных сбоев — хорошая новость.
Плохая новость: это значит, что одно событие может прийти дважды. Всегда проверяйте, не обработан ли уже данный invoice_id. В коде выше это сделано через processed_invoices set — в реальном продакшене используйте таблицу в базе данных или Redis с TTL.
Идемпотентность
Правило простое: обработка одного и того же webhook дважды не должна создавать дублей. Два варианта реализации:
- Upsert вместо insert:
INSERT INTO orders ... ON CONFLICT (invoice_id) DO NOTHING - Явная проверка: перед обработкой ищем
invoice_idв таблицеprocessed_webhooks
Истёкшие счета (expired)
Счёт в Kaspi живёт ограниченное время (обычно 15 минут). Если клиент не успел оплатить — AiPay пришлёт "status": "expired". Типичная реакция: уведомить клиента и предложить создать новый счёт.
Timeout вашего обработчика
AiPay ждёт ответа ограниченное время. Если ваша бизнес-логика (отправка email, обращение к внешнему API) занимает больше 5 секунд — вынесите её в фоновую задачу. Webhook-обработчик должен как можно быстрее вернуть 200 OK, а тяжёлую работу сделать асинхронно.
Попробуйте AiPay — 7 дней бесплатно
Если вы строите что-то с оплатой через Kaspi и хотите уйти от ручной проверки или polling, AiPay решает эту задачу. Интеграция занимает около часа при наличии базового бэкенда.
Пробный период — 7 дней, без ограничений по функциям. Стоимость после — ₸25 000 в месяц за терминал.
Если нужна помощь с архитектурой интеграции или есть вопросы — напишите нам, разберёмся.
Часто задаваемые вопросы
А если мой сервер упал в момент доставки webhook?
AiPay автоматически повторит попытку доставки. Логика retry обеспечивает, что при кратковременном сбое вашего сервера (перезапуск, деплой, перегрузка) событие не потеряется. На стороне вашего кода единственное требование — реализовать идемпотентность, чтобы повторная доставка не привела к дублированию заказа.
Дополнительная страховка: метод GET /invoices/{invoice_id} позволяет запросить текущий статус счёта в любой момент — хорошо использовать его при старте сервиса для синхронизации состояния.
Можно ли использовать без сервера (no-code / low-code)?
Технически webhook требует публично доступного HTTP-эндпоинта. Но существуют варианты без написания полноценного сервера:
- Make (ex-Integromat) / n8n — визуальные автоматизации с поддержкой webhook-триггеров. Можно поднять за 30 минут без кода.
- Zapier — аналогично, есть webhook-триггер.
- Serverless functions — Vercel Functions, AWS Lambda, Cloudflare Workers. Минимальный код, нет необходимости в постоянно работающем сервере.
Для Telegram-ботов на Python популярный вариант — aiogram или python-telegram-bot с отдельным Flask/FastAPI-сервисом для webhook.
Как тестировать вебхуки локально?
Локальный сервер не виден из интернета, поэтому AiPay не сможет на него достучаться. Есть два удобных решения:
ngrok — самый популярный инструмент. Запускаете:
ngrok http 8000
Получаете публичный URL вида https://abc123.ngrok.io — указываете его как webhook URL в настройках AiPay. Все запросы проксируются на ваш локальный сервер.
Sandbox-окружение — AiPay предоставляет тестовое окружение, где вы можете создавать счета и симулировать платежи без реальных денег. Идеально для разработки и CI/CD-пайплайнов.
Нужно ли что-то настраивать в личном кабинете Kaspi?
Нет — всё взаимодействие идёт через AiPay. Вам нужно только зарегистрироваться на aipay.kz, получить API-ключ и WEBHOOK_SECRET, и указать ваш webhook URL в настройках кабинета AiPay. Прямой настройки в системах Kaspi не требуется.
Полный справочник API с описанием всех параметров, кодов ошибок и примерами для разных языков — на странице для разработчиков.
Готовы автоматизировать Kaspi Pay?
Подключитесь за 1 час. 7 дней бесплатно.
Попробовать AiPay бесплатно