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

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

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

Партнёр-интегратор (CRM, платформа, SaaS) одним ключом X-Partner-Key подключает N мерчантов: у каждого — своя организация в ApiPay и свой персональный X-API-Key, которым партнёр выставляет счета от имени этого мерчанта. Граница доверия — Kaspi-SMS кассиру: мерчант подтверждает подключение кодом, приходящим на его номер кассира. Деньги идут напрямую на Kaspi-счёт мерчанта. Начните с песочницы: весь онбординг там детерминирован и не трогает реальный Kaspi.

Предусловия

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

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

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

Базовый 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)

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

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

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

Шаг 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 (защита от того, чтобы случайный retry не уничтожил рабочую связку).
  • 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, затем повторить
409 no_process авторизация не начата вызвать init
409 context_expired Kaspi-контекст протух (process_id ~10 мин) вызвать init заново, повторить send-phone
409 org_claim_conflict этот кассир уже привязан к другой организации в ApiPay привязать нельзя; разобраться, где кассир уже подключён
502 sms_failed Kaspi не вернул экран ввода OTP повторить позже
503 kaspi_busy Kaspi временно недоступен / троттлит повторить примерно через минуту

Лимит partner-kaspi-auth — 10 req/min (боевая организация) / 60 req/min (тестовая — мок не шлёт SMS) на партнёра и организацию. На 503 kaspi_busy просто выждите паузу перед повтором; не завязывайте жёсткую логику ровно на заголовок Retry-After.

Шаг 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 org_claim_conflict — кассир/организация уже привязаны к другой организации в ApiPay.
  • 409 no_process — авторизация не начата или истекла (вернитесь к init).
  • 502 finish_no_x509 / finish_no_token_sn / http_error — внутренняя ошибка финализации Kaspi-сессии; повторить позже, при повторе — в поддержку.

Шаг 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 (TTL 5 минут). В 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).

Справочник ошибок по шагам

Якорная сводка «шаг → код → HTTP → что делать»:

Шаг error HTTP Что делать
init already_connected 409 повторить init с "force": true
init/send-phone/verify-otp production_access_required 403 нужен production-режим (см. ниже)
send-phone invalid_phone 422 исправить формат 7XXXXXXXXXX
send-phone not_cashier / not_registered 422 проблема регистрации кассира в Kaspi — к мерчанту
send-phone context_expired 409 init заново, повторить send-phone
send-phone / verify-otp org_claim_conflict 409 кассир занят другой организацией — разобраться
send-phone sms_failed / kaspi_busy 502 / 503 повторить позже (примерно через минуту)
verify-otp invalid_otp 200 код неверный — процесс жив, повторить verify-otp
verify-otp finish_no_x509 / finish_no_token_sn / http_error 502 повторить; при повторе — в поддержку

Переход в production

Sandbox доступен сразу и self-service. 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 за подписку подключённого мерчанта (start/business/pro) — это подписочная плата мерчант→ApiPay, а не оборот мерчанта. Оплата идёт счётом через Kaspi (телефон плательщика в теле, активация асинхронная после оплаты); триал 3 дня сохраняется, оплата продлевает от конца триала или периода. Полный разбор тарифных эндпоинтов и кодов — в парной статье про встраивание Partner API и на странице apipay.kz/partner-api.

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

GET /api/partner/health возвращает агрегат по всем организациям партнёра (кэш ~30 секунд): блок organizations с total/kaspi_connected/needs_reauth/tariff_active/tariff_expired/on_trial, блок webhooks (delivered_24h/failed_24h/success_rate) и rate_limits. Если 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.

Неверный код из SMS вернул HTTP 200 — это баг?

Нет. invalid_otp приходит именно с HTTP 200 и {"success":false}; сессия авторизации не сбрасывается. Запросите код заново и повторите verify-otp — процесс жив. Не трактуйте это как транспортную ошибку.

Почему хост — api.apipay.kz, а не apipay.kz?

apipay.kz — это сайт и SPA-кабинет. Весь S2S Partner API по X-Partner-Key живёт на https://api.apipay.kz/api/partner, а мерчантский платёжный — на https://api.apipay.kz/api/v1.

Что делать при 403 production_access_required?

Вы пытаетесь работать с боевой организацией, будучи в sandbox-режиме. Production открывается после ручного одобрения администратором (коммерческий договор); переключение режима — в веб-кабинете партнёра. В песочнице тот же флоу доступен сразу.

Повторный POST /organizations создаст дубликат?

Нет, если передать тот же external_id: вызов идемпотентен и вернёт уже существующую организацию с HTTP 200. external_id — ваш ключ идемпотентности и референс клиента в CRM.

Как понять, что мерчанту нужно переподключить кассира?

По GET /api/partner/health: если needs_reauth > 0, сверьтесь по организации через kaspi-auth/status. Переподключение — init с "force": true, затем send-phone и verify-otp.

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

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

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

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