Модель встройки
Ключевое отличие от обычной интеграции: у мерчанта нет своего аккаунта 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-партнёру (referral → 403 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) и должен быть постоянным доменом вашего сервиса, не шортенером и не временной заглушкой. Ручка отдаёт200и при первой выдаче, и при ротации — не проверяйте на201. - Ротация per-org ключа — повторный тот же вызов (идемпотентно,
regenerated: true) перегенерирует ключ той же записи; старый ключ умирает мгновенно. Обновляйте ключ в своей базе атомарно. - Ротация партнёрского
X-Partner-Keyи переключение sandbox↔production делаются в веб-кабинете партнёра (детали кабинета — вне этой статьи). - Хранение у интегратора.
X-Partner-Key— один на всю интеграцию, в секрет-хранилище бэкенда. Per-orgX-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 ещё не вызван — финальный статус придёт вебхуком или поллингом GET /api/v1/invoices/{id}. Не пересоздавайте счёт, пока он в processing, — получите два живых счёта.
QR-счёт — POST /api/v1/invoices/qr → 201 сразу status: "pending" + QR-поля. Срок жизни берите из qr_expires_at, а не из константы в коде: окно и лимиты разобраны в статье «QR-счёт: окно и лимиты».
Статусы счёта и их разбор — в статье «Жизненный цикл счёта». Для интеграции важно одно: реагируйте на последний статус, а не на ожидаемый порядок — легитимны и 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, secret). Верифицируйте по сырым байтам тела запроса, до JSON-парсинга; пересериализация JSON ломает подпись. Готовые приёмники на PHP/Node/Python, ретраи и circuit breaker — в статье «Настройка вебхуков ApiPay».
Партнёрская специфика — в выборе секрета: события счетов подписаны webhook_secret того per-org ключа, на чей webhook_url они пришли, а события на партнёрский webhook_url (tariff.activated, тестовый webhook.test) — партнёрским webhook_secret, который выдаётся вместе с X-Partner-Key и ротируется отдельно. Считать партнёрское событие per-org секретом бесполезно — подпись не сойдётся. Если партнёрский секрет не задан, заголовка X-Webhook-Signature на партнёрских событиях не будет вовсе.
Даты — ISO 8601 с явным смещением, парсите строку целиком вместе со смещением.
- Мерчантский API (
/api/v1): счета, возвраты, подписки, каталог, подключения и вебхуки счетов — UTC (+00:00);GET /tariff,GET /account/healthиperiod.start/period.endв/invoices/stats,/refunds/stats— Asia/Almaty (+05:00). - Partner API (
/api/partner):+05:00во всех ответах, включая payload партнёрских вебхуковtariff.activated/webhook.test. - Параметры фильтров
date_from/date_toбез явного смещения трактуются в Asia/Almaty.
Возвраты — POST /api/v1/invoices/{id}/refund → 201 и асинхронный результат вебхуком invoice.refunded. Полный разбор — «Возвраты Kaspi через API».
Жизненный цикл мерчанта
- Онбординг. Создать организацию → авторизовать кассира по SMS → выдать
X-API-Key. Кратко:POST /organizations→kaspi-auth/init→send-phone→verify-otp→POST /organizations/{id}/api-key. Требования к кассиру и пошаговый онбординг — на странице Partner API и в «Подключение кассира». - Триал. После успешной авторизации кассира мерчант получает авто-триал — см. «Пробный период».
- Тариф-биллинг партнёром (ниже).
- Мониторинг health —
GET /api/partner/health(ниже). - Переподключение кассира (если
needs_reauth > 0) —kaspi-auth/initс"force": true→send-phone→verify-otp(уложитесь в отведённое окно авторизации).
Тариф-биллинг. Партнёр сам платит ApiPay за подписку мерчанта (start/business/pro/pro_max). Это подписочная плата мерчант→ApiPay, а не оборот мерчанта — оборот партнёру не отдаётся, биллинг живёт отдельным эндпоинтом. Оплата оформляется счётом через Kaspi, активация асинхронная после оплаты.
GET /organizations/{id}/tariff— снимок подписки. Нет тарифа →status: "none"(не404).GET /tariff-plans— общий каталог тарифов и планов (периодыperiod_months∈ 1/3/6/12). Точную сумму к оплате за выбранный период возвращает сервер — не считайте её на своей стороне; сетка тарифов и дневные лимиты — в статьях «Тарифы и комиссия» и «Лимит счетов по тарифу».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 (магические тестовые значения — на странице Partner API). Production — реальные Kaspi/SMS/OTP; требует ручного одобрения (api_access_status: granted).
Пока партнёр в sandbox-режиме, любой S2S-вызов по боевой организации (kaspi-auth/*, tariff/pay) отбивается 403 production_access_required. Переключение режима — в веб-кабинете партнёра; переход в production хард-удаляет все тестовые организации партнёра вместе с их ключами (подробнее — «Песочница и рабочий режим»). Лимиты песочницы: 20 тестовых организаций на партнёра (429 test_org_limit), 1000 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). - [ ] Ветки отказа авторизации кассира —
409 cashier_unavailable, суточный429 rate_limited, терминальные исходы: разобраны в «Как партнёру подключить организацию». - [ ] Безопасное хранение 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-код. - [ ] Реакция на последний статус счёта, а не на ожидаемый порядок.
- [ ] Мониторинг через
GET /health(needs_reauth,webhooks.success_rate) + план переавторизации кассира.
Частые вопросы
Нужен ли мерчанту свой аккаунт ApiPay?
Нет. Мерчант не заводит аккаунт: партнёр онбордит его организацию через Partner API и хранит выданный per-org X-API-Key у себя. Мерчант лишь подтверждает код из Kaspi-SMS при авторизации кассира.
Каким секретом проверять подпись вебхука?
Секретом того канала, куда пришло событие: счета и возвраты — webhook_secret per-org ключа мерчанта, tariff.activated и webhook.test на партнёрском webhook_url — партнёрским webhook_secret. Один общий секрет не подойдёт.