> Источник: https://apipay.kz/guides/partner-connect-organization · Обновлено: 2026-07-06 · apipay.kz
> ApiPay — независимый сервис приёма платежей поверх вашего Kaspi Pay.

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

**TL;DR.** Один партнёрский ключ `X-Partner-Key` онбордит сколько угодно мерчантов через server-to-server Partner API на хосте `https://api.apipay.kz/api/partner`. Путь одного мерчанта — 7 шагов: создать организацию → начать авторизацию кассира (`init`) → отправить номер кассира (`send-phone`) → подтвердить код из SMS (`verify-otp`) → дождаться готовности (`status`) → выдать мерчанту его персональный `X-API-Key` → выставить первый счёт. Сначала весь путь прогоняется в **sandbox** — без единого реального Kaspi-вызова и без SMS, на магических значениях (OTP `0000`, кассир-номера `77770000010…015`). Тот же код работает в production, отличаются только магические значения на реальные.

## Коротко

| Параметр | Значение |
|---|---|
| Хост Partner API (S2S) | `https://api.apipay.kz/api/partner` |
| Хост мерчантского API | `https://api.apipay.kz/api/v1` |
| Заголовок партнёра | `X-Partner-Key` (только у operating-партнёра) |
| Заголовок мерчанта | `X-API-Key` (выдаётся на каждую организацию) |
| Окно авторизации | `process_id` живёт ~10 минут — уложите `send-phone` + `verify-otp` в него |
| Идемпотентность организации | По `external_id` — повтор вернёт ту же организацию |

## Предусловия

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

- **Партнёрский `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`:

```json
{
  "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-ключ и вебхук-секрет](/guides/api-klyuch-i-webhook-secret).

## Шаг 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-счёт: окно и лимиты](/guides/qr-schet-ttl-i-limity). В sandbox опциональное поле `"simulate": "paid|cancelled|expired"` сразу финализирует QR.

Полный мерчантский платёжный API (все поля, статусы, отмены, возвраты, вебхуки) — в мерчантской документации [apipay.kz/docs](/docs), а как создавать счета по номеру — в разборе [счёт Kaspi по номеру](/guides/kak-sozdat-schet-kaspi-po-nomeru).

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

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

```bash
# 1. Создать тестовую организацию
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"}'
# 201 {"success":true,"organization":{"id":501,"status":"pending","sandbox_mode":true,...}}

# 2. init — начать авторизацию кассира
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 '{}'
# 200 {"success":true,"process_id":"SANDBOX-...","process_status":"phone_required"}

# 3. send-phone — магический номер кассира = успех
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"}'
# 200 {"success":true,"process_status":"otp_required"}

# 4. verify-otp — сначала неверный код (повторяемо), потом магический 0000
curl -X POST .../kaspi-auth/verify-otp -H 'X-Partner-Key: YOUR_PARTNER_KEY' \
  -H 'Content-Type: application/json' -d '{"otp":"1234"}'
# 200 {"success":false,"error":"invalid_otp","process_status":"otp_required"}
curl -X POST .../kaspi-auth/verify-otp -H 'X-Partner-Key: YOUR_PARTNER_KEY' \
  -H 'Content-Type: application/json' -d '{"otp":"0000"}'
# 200 {"success":true,"organization":{"status":"verified",...}}

# 5. Выдать мерчанту X-API-Key
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"}'
# 200 {"success":true,"key":"<X-API-Key один раз>","webhook_secret":"<один раз>","is_org_default":true,...}

# 6. Первый счёт — уже выданным X-API-Key, плательщик = 8XXXXXXXXXX
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"}'
# 201 status=pending, is_sandbox=true

# 7. Симулировать оплату → на webhook_url прилетит invoice.status_changed
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"}'
# 200 payload вебхука идентичен боевому
```

Магические значения 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. Про несколько организаций на аккаунт и тарифы — на странице [для партнёров](/partners).

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

Партнёр сам платит ApiPay за подписку подключённого мерчанта — это подписочная плата мерчант→ApiPay, а не оборот мерчанта. Разбор тарифных эндпоинтов — в статье [Partner API white-label](/guides/partner-api-white-label), сетка тарифов — в [Тарифы и комиссия ApiPay](/guides/tarify-i-komissiya-apipay).

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

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

Переподключение кассира = повтор онбординг-шагов с `init` + `"force": true` → `send-phone` → `verify-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`.

---

ApiPay — независимый сервис и не аффилирован с АО «Kaspi Bank». Kaspi и Kaspi Pay — товарные знаки их правообладателя.
База знаний: https://apipay.kz/guides
