Как встроить приём Kaspi в свой продукт через Partner API?

Обновлено 6 июля 2026 · Для вашего бизнеса · Версия в Markdown
Содержание
  1. Модель встройки
  2. Два ключа, две границы
  3. Поток счёта и вебхука
  4. Жизненный цикл мерчанта
  5. Sandbox → Production
  6. Чек-лист готовности встройки
  7. Частые ошибки
  8. Частые вопросы

Модель встройки

Ключевое отличие от обычной интеграции: у мерчанта нет своего аккаунта ApiPay. Партнёр на своей стороне онбордит мерчанта (авторизует его кассира Kaspi по SMS) и получает для него per-org X-API-Key + вебхук, которыми создаёт счета через публичный API.

Сущности и владение:

  • Партнёр привязан к одному User (partners.user_id).
  • Все организации онбордящихся мерчантов принадлежат этому User. Это и есть граница авторизации: партнёр работает только со своими организациями. Чужая или несуществующая организация → 404 organization_not_found (существование чужих организаций не раскрывается).

Дерево сущностей словами:

Партнёр (1 × X-Partner-Key)
└── организации мерчантов (N)
    └── у каждой: per-org X-API-Key (+ webhook_url + webhook_secret)

Деньги при этом идут напрямую с Kaspi покупателя на Kaspi-счёт мерчанта. Промежуточного счёта у партнёра нет — вы автоматизируете выставление счетов, а не аккумулируете чужие платежи.

Два ключа, две границы

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

X-Partner-Key per-org X-API-Key
Владелец партнёр конкретный мерчант (ключ выдаёт партнёр)
Тип server-to-server обычный публичный API-ключ
Хост https://api.apipay.kz/api/partner https://api.apipay.kz/api/v1
Для чего онбординг организаций, авторизация кассира, выдача ключей, тариф, health счета, статусы, возвраты, каталог, lookup
Где взять в веб-кабинете партнёра POST /organizations/{id}/api-key
Хранение хеш в БД, показывается один раз хеш в БД, key показывается один раз
Доступ только operating-партнёру (referral403 forbidden) активному ключу активной организации

Выдача per-org ключа — POST /organizations/{id}/api-key:

curl -X POST "https://api.apipay.kz/api/partner/organizations/50/api-key" \
  -H "X-Partner-Key: YOUR_PARTNER_KEY" -H "Content-Type: application/json" \
  -d '{"webhook_url":"https://your-crm.example.com/webhooks/kaspi"}'
  • One-time. key и webhook_secret возвращаются в открытом виде ровно один раз — сохраните оба сразу, иначе понадобится перевыпуск. webhook_url обязателен и проходит SSRF-валидацию: приватный адрес → 422.
  • Ротация per-org ключа — повторный тот же вызов (идемпотентно, regenerated: true) перегенерирует ключ той же записи; старый ключ умирает мгновенно. Обновляйте ключ в своей базе атомарно.
  • Ротация партнёрского X-Partner-Key и переключение sandbox↔production делаются в веб-кабинете партнёра (детали кабинета — вне этой статьи).
  • Хранение у интегратора. X-Partner-Key — один на всю интеграцию, в секрет-хранилище бэкенда. Per-org X-API-Key и webhook_secret — по одному на организацию мерчанта, привязанные к его записи в вашей CRM. Никогда не кладите ключи в клиентский/браузерный код.

Полный мерчантский платёжный API (все методы X-API-Key) документирован в спецификации API — не воспроизводите его целиком, ссылайтесь. Разбор пары «ключ vs секрет» — «API-ключ и вебхук-секрет».

Поток счёта и вебхука

Создание счёта от имени мерчанта — выданным X-API-Key против /api/v1:

curl -X POST "https://api.apipay.kz/api/v1/invoices" \
  -H "X-API-Key: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{"phone_number":"8XXXXXXXXXX","amount":5000,"description":"Заказ №123"}'

201 со status: "processing" — это не ошибка: Kaspi ещё не вызван, статус доедет вебхуком или поллингом. QR-счёт — POST /api/v1/invoices/qr201 сразу status: "pending" + QR-поля (TTL 5 минут). Не пересоздавайте счёт, пока он в processing, — получите два живых счёта.

Статусы счёта:

Статус Смысл Терминальный
processing создан, ждёт обработки нет
pending отправлен в Kaspi, ждёт оплату нет
cancelling запрошена отмена нет
paid оплачен да
cancelled отменён да
expired истёк да
error техническая ошибка да
partially_refunded частично возвращён да

Легитимные «странные» последовательности — их надо уметь обрабатывать, это не баг: cancelled → paid и expired → paid (клиент оплатил в последний момент, оплата выигрывает гонку), error → pending (реконсиляция — счёт на самом деле прошёл), paid → partially_refunded (первый частичный возврат). Реагируйте на последний статус, а не на ожидаемый порядок.

Куда приходят вебхуки. На webhook_url per-org X-API-Key (ключа-создателя счёта; плюс копия на org-default-ключ организации, если это другой ключ — до двух получателей). Партнёрский webhook_url в доставке invoice/refund не участвует — вебхуки счетов конкретного мерчанта идут на вебхук этого мерчанта. Релевантные события: invoice.status_changed, invoice.refunded, invoice.qr_scanned (технические processing/cancelling вебхуков не порождают).

Подпись — HMAC по сырому телу. Заголовок X-Webhook-Signature: sha256=<hex>, где <hex> = HMAC-SHA256(raw body, webhook_secret). Верифицируйте по сырым байтам тела запроса, до JSON-парсинга; пересериализация JSON ломает подпись.

// Node — проверка подписи вебхука
const crypto = require("crypto");
const expected = "sha256=" + crypto
  .createHmac("sha256", process.env.YOUR_WEBHOOK_SECRET)
  .update(rawBody)                 // Buffer с сырым телом, до JSON.parse
  .digest("hex");
const got = Buffer.from(req.get("X-Webhook-Signature") || "");
const exp = Buffer.from(expected);
if (got.length !== exp.length || !crypto.timingSafeEqual(exp, got)) return res.status(401).end();
import hmac, hashlib, os
expected = "sha256=" + hmac.new(
    os.environ["YOUR_WEBHOOK_SECRET"].encode(), raw_body, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, sig_header):
    abort(401)

Готовые copy-paste приёмники на PHP/Node/Python, ретраи и circuit breaker — в статье «Настройка вебхуков ApiPay».

Даты. Все таймстампы внутри webhook-payload — ISO 8601 в UTC (+00:00), тогда как даты в HTTP-ответах API отдаются в +05:00 (Asia/Almaty). Это различие легко упустить при сверке времени.

ВозвратыPOST /api/v1/invoices/{id}/refund { "amount": 2000, "reason": "..." }201 (refund.status: pending) → асинхронно → вебхук invoice.refunded со status completed/failed (+ error_code при неудаче). Статуса refunded у счёта нет: после полного возврата счёт остаётся paid/partially_refunded с is_fully_refunded: true.

Жизненный цикл мерчанта

  1. Онбординг. Создать организацию → авторизовать кассира по SMS → выдать X-API-Key. Кратко: POST /organizationskaspi-auth/initsend-phoneverify-otpPOST /organizations/{id}/api-key. Требования к кассиру и пошаговый онбординг — на странице Partner API и в «Подключение кассира».
  2. Триал. После успешной авторизации кассира обычный мерчант получает авто-триал 3 дня; он сохраняется при последующей оплате тарифа (оплата продлевает срок от конца триала).
  3. Тариф-биллинг партнёром (ниже).
  4. Мониторинг healthGET /api/partner/health (ниже).
  5. Переподключение кассира (если needs_reauth > 0) — kaspi-auth/init с "force": truesend-phoneverify-otp (уложитесь в отведённое окно авторизации).

Тариф-биллинг. Партнёр сам платит ApiPay за подписку мерчанта (start/business/pro). Это подписочная плата мерчант→ApiPay, а не оборот мерчанта — оборот партнёру не отдаётся, биллинг живёт отдельным эндпоинтом. Оплата оформляется счётом через Kaspi, активация асинхронная после оплаты.

  • GET /organizations/{id}/tariff — снимок подписки. Нет тарифа → status: "none" (не 404).
  • GET /tariff-plans — общий каталог тарифов и планов (периоды period_months ∈ 1/3/6/12). Тарифы: start (10 000 ₸/мес, до 30 счетов в день), business (25 000 ₸/мес, до 100), pro (60 000 ₸/мес, 100–300; больше 300 в день — договорные условия). Точную сумму к оплате за выбранный период возвращает сервер — не считайте её на своей стороне.
  • POST /organizations/{id}/tariff/pay — оплатить тариф.
  • GET /organizations/{id}/tariff/payments и /{paymentId} — история платежей.
curl -X POST "https://api.apipay.kz/api/partner/organizations/50/tariff/pay" \
  -H "X-Partner-Key: YOUR_PARTNER_KEY" -H "Content-Type: application/json" \
  -d '{"tier_id":"business","period_months":3,"phone":"8XXXXXXXXXX"}'

phone — плательщик (формат 8XXXXXXXXXX), не кассир. Боевая организация → 201, payment.status: "pending", реальный Kaspi-счёт (self_api_invoice_id задан) — только для production-партнёра. Тестовая/sandbox-организация → мгновенная мок-активация: 201, payment.status: "completed", self_api_invoice_id: null. Ошибки оплаты: 422 invalid_tariff_plan, 409 tariff_payment_pending (уже есть живой неоплаченный счёт), 429 tariff_payment_cooldown (Retry-After + retry_after_seconds: 1800), 503 tariff_payment_unavailable (retry_after_seconds: 30, ретрай позже), 403 production_access_required (боевая организация у sandbox-партнёра).

Health — GET /api/partner/health (агрегат по всем организациям партнёра, кэш ~30 с):

{ "success": true, "api": {"status": "ok"},
  "organizations": {"total": 12, "kaspi_connected": 9, "needs_reauth": 1,
                    "tariff_active": 7, "tariff_expired": 2, "on_trial": 3},
  "webhooks": {"delivered_24h": 340, "failed_24h": 5, "success_rate": 98.6},
  "rate_limits": {"partner_api_per_min": 120} }

needs_reauth > 0 → требуется переподключение кассира. Детект — поллингом /health (и per-org kaspi-auth/status); отдельного вебхука об этом нет.

Sandbox → Production

Sandbox доступен сразу, self-service: весь онбординг, счета, вебхуки, возвраты и lookup работают end-to-end без реальных вызовов Kaspi и SMS, детерминированно — можно покрыть автотестами CRM (магические тестовые значения — на странице Partner API). Production — реальные Kaspi/SMS/OTP; требует ручного одобрения (коммерческий договор, api_access_status: granted).

Пока партнёр в sandbox-режиме, любой S2S-вызов по боевой организации (kaspi-auth/*, tariff/pay) отбивается 403 production_access_required. Переключение режима и запрос production-доступа делаются в веб-кабинете партнёра. Важно: переход в production хард-удаляет все тестовые организации партнёра вместе с их ключами — сохраните конфигурацию заранее (подробнее — «Песочница и рабочий режим»). Лимиты песочницы: 20 тестовых организаций на партнёра (429 test_org_limit), 500 sandbox-счетов на организацию (400 sandbox_invoice_limit).

Чек-лист готовности встройки

Перед запуском убедитесь, что в системе интегратора закрыт каждый пункт:

  • [ ] Идемпотентность создания счетов. Передавайте стабильный external_order_id_idempotency — не создавайте дубль-счёт на ретрае; при онбординге организации — external_id как ключ идемпотентности.
  • [ ] Обработка 429 / Retry-After / retry_after_seconds. Группа Partner API — 120 req/min на партнёра; создание организации — 10/min; авторизация кассира — 10/min (боевая) / 60/min (тест). Тарифные лимиты кладут retry_after_seconds в тело (1800/30). Уважайте паузы.
  • [ ] Безопасное хранение one-time секретов. key и webhook_secret показываются один раз — сохраните сразу в секрет-хранилище, не в браузере/логах.
  • [ ] Верификация HMAC по raw body. Отвергайте вебхуки с неверной подписью (401).
  • [ ] Дедуп вебхуков по (invoice.id, invoice.status) и (refund.id, refund.status); отвечайте 200 быстро (до ~5 с), обработку — асинхронно.
  • [ ] Все формы конверта ошибок и стабильные error_code. Контроллер — { success:false, error, error_code, message, errors? }; middleware — сокращённо { success:false, error }; валидация FormRequest — Laravel-форма { message, errors }. Читайте error_code, а не только HTTP-код.
  • [ ] Легитимные «странные» переходы статусов (cancelled→paid, expired→paid, error→pending, paid→partially_refunded) — реагируйте на последний статус.
  • [ ] Мониторинг через GET /health (needs_reauth, webhooks.success_rate) + план переавторизации кассира.

Частые ошибки

  • Ждать вебхуки счетов на партнёрский webhook_url. invoice/refund идут только на per-org webhook_url мерчанта (ключа-создателя + org-default). Партнёрский вебхук в этой доставке не участвует.
  • Путать ключи и хосты. X-Partner-Key.../api/partner; per-org X-API-Key.../api/v1. Ключ не с тем хостом или apipay.kz вместо api.apipay.kz — типовая причина 401.
  • Не сохранить one-time key/webhook_secret. Показываются один раз; не записали — только перевыпуск (старый ключ умирает).
  • Проверять HMAC по перепарсенному JSON. Только сырое тело; пересериализация меняет байты и ломает подпись.
  • Пересоздавать счёт в processing. Получите два живых счёта. Ждите вебхук или GET /invoices/{id}; защищайтесь идемпотентностью.
  • Дёргать operating-методы referral-партнёром. Онбординг, тариф и health доступны только operating-партнёру; у referral → 403 forbidden.

Частые вопросы

Нужен ли мерчанту свой аккаунт ApiPay?

Нет. Мерчант не заводит аккаунт: партнёр онбордит его организацию через Partner API и хранит выданный per-org X-API-Key у себя. Мерчант лишь подтверждает код из Kaspi-SMS при авторизации кассира.

На чей webhook_url приходят вебхуки счетов?

На per-org webhook_url организации мерчанта (ключа, создавшего счёт, плюс org-default-ключ, если это другой ключ). Партнёрский webhook_url в доставке invoice/refund не участвует.

Чем тариф-биллинг отличается от оборота мерчанта?

Тариф — подписочная плата мерчант→ApiPay (start/business/pro), которую партнёр оплачивает отдельным эндпоинтом. Оборот мерчанта (деньги покупателей) идёт напрямую на его Kaspi-счёт и партнёру не отдаётся.

Когда требуется переподключение кассира мерчанта?

Когда GET /health показывает needs_reauth > 0. Переавторизуйте привязку: kaspi-auth/init с "force": truesend-phoneverify-otp. Отдельного вебхука об этом нет — отслеживайте поллингом.

Чем sandbox отличается от production для встройки?

Sandbox — детерминированные моки без реальных Kaspi/SMS, доступен сразу, годится для автотестов CRM. Production — реальные операции, после ручного одобрения. Боевые вызовы у sandbox-партнёра отбиваются 403 production_access_required.

Интеграция с ИИ

Не настраивайте вручную — поручите ИИ

Скопируйте готовый промпт и вставьте своему ИИ-ассистенту — он подключит приём платежей примерно за 15 минут. Настраивает ИИ, а не вы.

Для разработчика. Полная спецификация API — apipay.kz/docs. Эта статья в Markdown — /guides/partner-api-white-label.md. Справочник фактов для ИИ-агента — apipay.kz/llms.txt.