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

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

**TL;DR.** Partner API — это server-to-server интерфейс для платформ (CRM, SaaS, маркетплейс): **1 партнёрский аккаунт держит N организаций мерчантов**, у каждой свой `X-API-Key` и вебхук, приём Kaspi встраивается под вашим брендом. Сам мерчант отдельного аккаунта ApiPay не заводит — всё идёт через партнёра. Держите в голове **два ключа с разными границами**: партнёрский `X-Partner-Key` (онбординг, тариф, health) и per-org `X-API-Key` (счета, статусы, возвраты). Деньги идут напрямую на Kaspi-счёт мерчанта — ApiPay независимый сервис поверх его Kaspi Pay и к деньгам доступа не имеет. Хост S2S — `https://api.apipay.kz/api/partner`, платёжный — `https://api.apipay.kz/api/v1`.

## Коротко

| Вопрос | Ответ |
|---|---|
| Модель | 1 партнёр → N организаций мерчантов → per-org `X-API-Key` (+ webhook) |
| Аккаунт мерчанта | Нет: мерчант не заводит ApiPay-аккаунт, всё идёт через партнёра |
| `X-Partner-Key` | Ключ партнёра (S2S): онбординг, авторизация кассира, выдача ключей, тариф, health |
| per-org `X-API-Key` | Ключ мерчанта: счета, статусы, возвраты, каталог, lookup |
| Хосты | S2S — `.../api/partner`; платёжный — `.../api/v1` (не `apipay.kz`) |
| Вебхуки счетов | На per-org `webhook_url` мерчанта, **не** на партнёрский |
| Тариф | Партнёр сам платит ApiPay за подписку мерчанта (`start`/`business`/`pro`) |
| Лимиты | Partner API — 120 req/min; песочница — 20 тест-организаций, 500 счетов/орг |

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

Ключевое отличие от обычной интеграции: **у мерчанта нет своего аккаунта 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`:**

```bash
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"}'
# 201 {
#   "success": true, "key": "...", "key_id": 123,
#   "webhook_url": "https://your-crm.example.com/webhooks/kaspi",
#   "webhook_secret": "whsec_...", "is_org_default": true, "regenerated": false
# }
```

- **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](/docs) — не воспроизводите его целиком, ссылайтесь. Разбор пары «ключ vs секрет» — «[API-ключ и вебхук-секрет](/guides/api-klyuch-i-webhook-secret)».

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

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

```bash
# счёт по номеру (обработка асинхронная)
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 { "id": 1001, "status": "processing", ... }
```

`201` со `status: "processing"` — это **не ошибка**: Kaspi ещё не вызван, статус доедет вебхуком или поллингом. QR-счёт — `POST /api/v1/invoices/qr` → `201` сразу `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 ломает подпись.

```js
// 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();
```

```python
# Python — то же самое
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](/guides/nastroyka-webhookov-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 /organizations` → `kaspi-auth/init` → `send-phone` → `verify-otp` → `POST /organizations/{id}/api-key`. Требования к кассиру и пошаговый онбординг — на странице [Partner API](/partners) и в «[Подключение кассира](/connect-cashier)».
2. **Триал.** После успешной авторизации кассира обычный мерчант получает авто-триал **3 дня**; он сохраняется при последующей оплате тарифа (оплата продлевает срок от конца триала).
3. **Тариф-биллинг партнёром** (ниже).
4. **Мониторинг health** — `GET /api/partner/health` (ниже).
5. **Переподключение кассира** (если `needs_reauth > 0`) — `kaspi-auth/init` с `"force": true` → `send-phone` → `verify-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}` — история платежей.

```bash
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 с):

```json
{ "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](/partners)). **Production** — реальные Kaspi/SMS/OTP; требует ручного одобрения (коммерческий договор, `api_access_status: granted`).

Пока партнёр в sandbox-режиме, любой S2S-вызов по **боевой** организации (`kaspi-auth/*`, `tariff/pay`) отбивается `403 production_access_required`. Переключение режима и запрос production-доступа делаются в веб-кабинете партнёра. **Важно:** переход в production **хард-удаляет все тестовые организации** партнёра вместе с их ключами — сохраните конфигурацию заранее (подробнее — «[Песочница и рабочий режим](/guides/pesochnitsa-i-rabochiy-rezhim)»). Лимиты песочницы: 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": true` → `send-phone` → `verify-otp`. Отдельного вебхука об этом нет — отслеживайте поллингом.

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

Смотрите также: [Partner API](/partners) · [Подключение кассира](/connect-cashier) · [Настройка вебхуков ApiPay](/guides/nastroyka-webhookov-apipay) · [Песочница и рабочий режим](/guides/pesochnitsa-i-rabochiy-rezhim) · [API-ключ и вебхук-секрет](/guides/api-klyuch-i-webhook-secret) · пиллар «[Как принимать оплату Kaspi через API](/kaspi-api)».

---

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