Как партнёру подключить организацию мерчанта к ApiPay

Обновлено 6 июля 2026 · Для вашего бизнеса · Версия в Markdown
Содержание
  1. Предусловия
  2. Базовый URL и заголовок
  3. Сначала — прогон в sandbox
  4. Шаг 1. Создать организацию
  5. Шаг 2. Начать авторизацию кассира (init)
  6. Шаг 3. Отправить номер кассира (send-phone)
  7. Шаг 4. Подтвердить код из SMS (verify-otp)
  8. Шаг 5. Дождаться готовности (status)
  9. Шаг 6. Выдать мерчанту его X-API-Key
  10. Шаг 7. Первый счёт от имени мерчанта
  11. Полный sandbox-прогон (E2E)
  12. Переход в production
  13. Тариф мерчанта (кратко)
  14. Мониторинг: нужно ли переподключение кассира
  15. Частые вопросы

Предусловия

Что нужно до старта:

  • Партнёрский X-Partner-Key. Выпускается в веб-кабинете партнёра. Sandbox-ключ доступен сразу, self-service; production — после ручного одобрения администратором (это коммерческий договор). Ключ показывается один раз, в БД хранится только его sha256-хеш. Доступен только operating-партнёру; у referral-партнёра S2S закрыт.
  • От мерчанта — телефон кассира Kaspi в формате 7XXXXXXXXXX.

Два разных телефона в двух форматах — не перепутайте. Телефон кассира (чья Kaspi-касса принимает оплату) — 7XXXXXXXXXX, regex ^7\d{10}$, идёт в send-phone → cashier_phone. Телефон плательщика (кому выставляете счёт, lookup) — 8XXXXXXXXXX, regex ^8\d{10}$, идёт в /api/v1/invoices → phone_number, clients/check → phone.

Базовый URL и заголовок

Каждый S2S-запрос идёт на https://api.apipay.kz/api/partner с заголовком X-Partner-Key: <ключ>. Это не apipay.kz — там живёт сайт и SPA-кабинет, а не S2S-хост.

Конверт ответа: успех — { "success": true, ... }; ошибка контроллера/сервиса — { "success": false, "error": "<code>", "error_code": "<code>", "message": "<ru>", "errors"?: {...} } (error дублирует error_code для обратной совместимости; errors — только на 422 из контроллерной проверки). Ошибки middleware (аутентификация/владение/production-гейт) — сокращённые: { "success": false, "error": "<code>" }. Ошибки валидации FormRequest — стандартная Laravel-форма { "message", "errors" } без success. Машинные коды в error/error_code стабильны — используйте их для локализации на стороне CRM.

Кросс-слойные ошибки (общие для всех шагов):

HTTP error Когда
401 partner_key_missing нет заголовка X-Partner-Key
401 invalid_partner_key ключ неверный или партнёр отключён
401 partner_user_missing у партнёра не привязан пользователь
403 forbidden партнёр не operating (S2S закрыт для referral-типа)
404 organization_not_found организация не принадлежит партнёру или не существует
422 { message, errors } ошибка валидации тела (дефолтная Laravel-форма)
429 Too Many Attempts. превышен лимит группы (заголовок Retry-After)

Сначала — прогон в sandbox

Песочница детерминирована, не делает реальных Kaspi-вызовов и не шлёт SMS — её можно покрыть автотестами CRM. Sandbox — на уровне партнёра: один X-Partner-Key и тумблер sandbox ⟷ production. Тестовая организация остаётся sandbox_mode: true даже после verify-otp, Kaspi-подключение у неё не создаётся.

Шаг 1. Создать организацию

POST /organizations. Тело (все поля опциональны): { "has_catalog": false, "external_id": "crm-client-42", "name": "ТОО Example" }.

  • external_id — ваш референс клиента в CRM и ключ идемпотентности: повторный POST с тем же external_id вернёт уже существующую организацию (200), дубль не создастся.
  • name (max:255) — если пустой, автогенерится и позже подменяется реальным именем из Kaspi на verify-otp.
  • has_catalog — для большинства интеграций false (простые счета amount + description).

Ответ 201 (создана) / 200 (идемпотентный повтор): { "success": true, "organization": { "id": 501, "status": "pending", "sandbox_mode": true, "external_id": "crm-client-42", "origin": "created", ... } }. Лимит partner-org-create — 10 req/min на партнёра; достигнут лимит тестовых организаций (20) → 429 test_org_limit.

Шаг 2. Начать авторизацию кассира (init)

POST /organizations/{id}/kaspi-auth/init. Тело: {} или { "force": true } (переавторизация поверх активной сессии — смена кассира / переподключение). Ответ: { "success": true, "process_id": "...", "process_status": "phone_required" }. process_id живёт ~10 минут — уложите следующие два шага в это окно.

Ошибки:

  • 409 already_connected — организация уже подключена к Kaspi. Чтобы переподключить, повторите init с "force": true.
  • 403 production_access_required — боевая организация у sandbox-партнёра (см. «Переход в production»).

Шаг 3. Отправить номер кассира (send-phone)

POST /organizations/{id}/kaspi-auth/send-phone. Тело: { "cashier_phone": "7XXXXXXXXXX" } (формат ^7\d{10}$ — это телефон кассира, не плательщика). Успех: { "success": true, "process_status": "otp_required" } — Kaspi отправляет SMS-код на номер кассира. Это самый «ошибкоёмкий» шаг, разберите каждую ветку:

HTTP error Смысл Что делать
422 invalid_phone неверный формат номера исправить формат на 7XXXXXXXXXX
422 not_cashier у номера нет роли «Кассир» в Kaspi уточнить у мерчанта корректный номер кассира
422 not_registered номер не зарегистрирован кассиром в Kaspi сессия закрыта: мерчанту добавить номер в Kaspi Pay → Настройки → Сотрудники с ролью «Кассир», затем начинать заново с init. Повторный send-phone вернёт 409 no_process
409 no_process авторизация не начата вызвать init
409 context_expired Kaspi-контекст протух (process_id ~10 мин) вызвать init заново, повторить send-phone
409 cashier_unavailable кассира сейчас нельзя подключить; причина не раскрывается повтор не поможет — направьте мерчанта в поддержку ApiPay
429 rate_limited за сутки с аккаунта пробовали слишком много разных номеров кассиров окно суточное: выждите время из Retry-After / retry_after_seconds — это реальное время до обнуления счётчика, обычно часы, а не привычная минута. Уже подключённые кассиры этого владельца в счётчик не входят, переавторизация рабочей точки лимитом не блокируется
502 sms_failed Kaspi не вернул экран ввода OTP повторить позже
503 kaspi_busy анти-абуз Kaspi сессия закрыта: пауза ~60 секунд собственным бэкоффом, затем новый init

Лимит на шаги авторизации кассира — 10 req/min (боевая организация) / 60 req/min (тестовая — мок не шлёт SMS) на партнёра и организацию.

Терминальные исходы. После not_registered, context_expired и kaspi_busy сессия авторизации закрыта: повторный send-phone вернёт 409 no_process, единственный путь дальше — новый init. Заголовок Retry-After приходит только с 429 rate_limited и тарифными лимитами; на kaspi_busy и прочих его нет — используйте собственный бэкофф с ориентиром 60 секунд.

Два разных 429 на этой ручке различаются по телу: поминутный лимит группы отдаёт {"message": "Too Many Attempts."} без error_code, суточный — error_code: "rate_limited" и retry_after_seconds. Суточную паузу применяйте только ко второму.

Исходы cashier_unavailable и rate_limited в sandbox не воспроизводятся: тестовая организация идёт мок-путём, и сессию мок не ведёт — после not_registered в песочнице следующий send-phone ответит успехом, а на боевой организации тот же цикл упрётся в 409 no_process. Заложите обработку заранее.

Шаг 4. Подтвердить код из SMS (verify-otp)

POST /organizations/{id}/kaspi-auth/verify-otp. Тело: { "otp": "1234" } (4–6 цифр, ^\d{4,6}$). Успех 200: { "success": true, "mode": "self", "organization": <card>, "process_status": "active" } — организация финализируется (status: "verified").

Неверный код — это тоже HTTP 200, и он повторяем. { "success": false, "error": "invalid_otp", "process_status": "otp_required" }. Сессия НЕ сбрасывается — просто попросите код заново и повторите verify-otp. Не трактуйте invalid_otp как транспортную ошибку и не начинайте процесс заново.

Другие ошибки:

  • 409 cashier_unavailable — то же, что на send-phone: повтор не поможет, мерчанта в поддержку ApiPay.
  • 409 no_process — авторизация не начата или истекла (вернитесь к init).
  • 502 — Kaspi-сессия не финализировалась (ответ Kaspi не дошёл или пришёл неполным); повторить позже, при повторе — в поддержку с organization_id и временем запроса.

Шаг 5. Дождаться готовности (status)

GET /organizations/{id}/kaspi-auth/status. Ответ: { "success": true, "status": "none|pending|active|expired", "process_status": "idle|phone_required|otp_required|active|failed", "kaspi_connected": bool, "expires_at": "...|null" }. Используйте для отслеживания хода авторизации и чтобы понять, нужно ли переподключение кассира (needs_reauth).

Шаг 6. Выдать мерчанту его X-API-Key

POST /organizations/{id}/api-key. Тело: { "name": "CRM key", "webhook_url": "https://...", "webhook_secret": "..." }. webhook_url обязателен и проходит SSRF-валидацию (приватные/внутренние адреса → 422); name и webhook_secret опциональны (webhook_secret сгенерируется автоматически). Вызов идемпотентен: повтор перегенерирует ключ той же записи (regenerated: true).

Указывайте постоянный домен своего сервиса — не шортенер и не временный адрес-перехватчик.

Ответ 200:

{
  "success": true,
  "key": "<X-API-Key в открытом виде — показывается ОДИН РАЗ>",
  "key_id": 200,
  "webhook_url": "https://crm.example.kz/sub/501/webhook",
  "webhook_secret": "<секрет в открытом виде — показывается ОДИН РАЗ>",
  "is_org_default": true,
  "regenerated": false
}

key и webhook_secret возвращаются в открытом виде ровно один раз — сохраните их сразу на своей стороне. is_org_default = true только если у организации ещё не было дефолтного ключа. Как ключ соотносится с секретом подписи вебхука — в разборе API-ключ и вебхук-секрет.

Шаг 7. Первый счёт от имени мерчанта

Дальше работаете выданным X-API-Key (не партнёрским ключом) против https://api.apipay.kz/api/v1:

  • Счёт по номеру — POST /api/v1/invoices, тело { "phone_number": "8XXXXXXXXXX", "amount": 5000, "description": "Заказ №123" }201 (в sandbox — status: pending, is_sandbox: true).
  • QR-счёт — POST /api/v1/invoices/qr, тело { "amount": 5000, "description": "..." }201 + qr_token_url, qr_image_url, qr_expires_at. Срок жизни берите из qr_expires_at, а не из константы в коде — разбор окна и лимитов в статье QR-счёт: окно и лимиты. В sandbox опциональное поле "simulate": "paid|cancelled|expired" сразу финализирует QR.

Полный мерчантский платёжный API (все поля, статусы, отмены, возвраты, вебхуки) — в мерчантской документации apipay.kz/docs, а как создавать счета по номеру — в разборе счёт Kaspi по номеру.

Полный sandbox-прогон (E2E)

Связный copy-paste сценарий от создания организации до симуляции оплаты. Плейсхолдеры — YOUR_PARTNER_KEY и YOUR_API_KEY:

curl -X POST https://api.apipay.kz/api/partner/organizations \
  -H 'X-Partner-Key: YOUR_PARTNER_KEY' -H 'Content-Type: application/json' \
  -d '{"has_catalog":false,"external_id":"crm-client-42"}'

curl -X POST https://api.apipay.kz/api/partner/organizations/501/kaspi-auth/init \
  -H 'X-Partner-Key: YOUR_PARTNER_KEY' -H 'Content-Type: application/json' -d '{}'

curl -X POST https://api.apipay.kz/api/partner/organizations/501/kaspi-auth/send-phone \
  -H 'X-Partner-Key: YOUR_PARTNER_KEY' -H 'Content-Type: application/json' \
  -d '{"cashier_phone":"77770000010"}'

curl -X POST .../kaspi-auth/verify-otp -H 'X-Partner-Key: YOUR_PARTNER_KEY' \
  -H 'Content-Type: application/json' -d '{"otp":"1234"}'
curl -X POST .../kaspi-auth/verify-otp -H 'X-Partner-Key: YOUR_PARTNER_KEY' \
  -H 'Content-Type: application/json' -d '{"otp":"0000"}'

curl -X POST https://api.apipay.kz/api/partner/organizations/501/api-key \
  -H 'X-Partner-Key: YOUR_PARTNER_KEY' -H 'Content-Type: application/json' \
  -d '{"name":"CRM key","webhook_url":"https://crm.example.kz/sub/501/webhook"}'

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":"87770001122","amount":5000,"description":"Заказ №123"}'

curl -X POST https://api.apipay.kz/api/v1/invoices/1001/simulate-status \
  -H 'X-API-Key: YOUR_API_KEY' -H 'Content-Type: application/json' -d '{"status":"paid"}'

Магические значения sandbox: OTP 0000 (успех); кассир-телефоны 77770000010 (успех), …011 (not_cashier), …012 (sms_failed), …013 (not_registered), …014 (context_expired), …015 (kaspi_busy); lookup-номера 87770000001 (есть Kaspi, «Иван И.»), 87770000002 (нет Kaspi).

Переход в production

Production-операции (реальный Kaspi-auth и боевые счета) открываются после ручного одобрения администратором (api_access_status = granted). Пока партнёр в sandbox-режиме, любой S2S-вызов по боевой организации (init/send-phone/verify-otp/status/tariff/pay) отбивается 403 production_access_required.

Переключение режима sandbox ⟷ production делается в веб-кабинете партнёра; переход в production хард-удаляет все тестовые организации партнёра. В production отличается только «магия»: реальные телефоны/SMS/OTP вместо 777…/0000 — флоу и коды ошибок идентичны sandbox. Про несколько организаций на аккаунт и тарифы — на странице для партнёров.

Тариф мерчанта (кратко)

Партнёр сам платит ApiPay за подписку подключённого мерчанта — это подписочная плата мерчант→ApiPay, а не оборот мерчанта. Разбор тарифных эндпоинтов — в статье Partner API white-label, сетка тарифов — в Тарифы и комиссия ApiPay.

Мониторинг: нужно ли переподключение кассира

GET /api/partner/health возвращает агрегат по всем организациям партнёра (разбор полей — в статье Partner API white-label). Если needs_reauth > 0 — требуется переподключение кассира: сверьтесь по конкретной организации через GET /organizations/{id}/kaspi-auth/status (kaspi_connected/status).

Переподключение кассира = повтор онбординг-шагов с init + "force": truesend-phoneverify-otp. Отдельного вебхука об этом нет — детектите поллингом /health и /status.

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

Чем X-Partner-Key отличается от X-API-Key?

X-Partner-Key — партнёрский ключ для S2S Partner API (/api/partner): онбординг организаций, тариф, health. X-API-Key — персональный ключ каждой организации мерчанта для обычного платёжного API (/api/v1): счета, вебхуки, возвраты. Перепутать их — главная причина 401.

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

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

Скопируйте готовый промпт и вставьте своему ИИ-ассистенту — он настроит интеграцию за вас: получит доступы, создаст тестовый счёт в песочнице и подключит вебхуки. Техническую часть делает ИИ, а не вы.

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