← Все статьи
вебхукиAPIразработка

Kaspi Pay webhook: что это и как настроить через AiPay

Разбираем, что такое webhook Kaspi Pay, почему его нет из коробки и как добавить event-driven уведомления об оплате через AiPay за один час.

Вы создали Kaspi-счёт. Клиент заплатил. Ваш код об этом не знает

Представьте типичную ситуацию: интернет-магазин на Казахстан, оплата через Kaspi. Клиент переводит деньги — и дальше один из трёх сценариев:

  1. Менеджер вручную смотрит выписку — раз в 5–10 минут, в рабочее время, по будням.
  2. Скрипт делает polling — бьёт в API каждые 30 секунд, 99% запросов впустую, сервер молотит вхолостую.
  3. Клиент пишет «я оплатил» — и вы верите на слово, пока не проверите.

Все три способа ломаются ночью, в выходные и при любой нагрузке выше «несколько заказов в день». Правильное решение — 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

  1. Клиент инициирует оплату — нажимает кнопку «Оплатить» в вашем интерфейсе (сайт, бот, приложение).
  2. Ваш бэкенд создаёт счёт — POST-запрос к AiPay с суммой и номером телефона клиента.
  3. AiPay создаёт счёт в Kaspi — клиент получает push-уведомление в приложении Kaspi с запросом на оплату.
  4. Клиент подтверждает платёж — одно нажатие в приложении Kaspi.
  5. AiPay получает подтверждение — фиксирует изменение статуса счёта.
  6. AiPay отправляет webhook — HTTP POST на ваш эндпоинт с данными о платеже и HMAC-подписью.
  7. Ваш сервер обрабатывает событие — проверяет подпись, проверяет идемпотентность, выполняет бизнес-логику.
  8. Заказ выполнен — товар выдан, статус обновлён, клиент доволен.

Весь цикл (шаги 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 защита:

  1. AiPay знает ваш WEBHOOK_SECRET (он генерируется при регистрации и хранится только у вас).
  2. При каждом webhook-запросе AiPay вычисляет HMAC-SHA256(тело_запроса, WEBHOOK_SECRET) и кладёт результат в заголовок X-Signature.
  3. Ваш сервер делает то же самое независимо и сравнивает результаты.
  4. Если подписи совпадают — запрос точно от 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 бесплатно