Если вы мерчант и хотите принимать платежи через Kaspi Pay в своём бизнесе — вам нужна обычная документация:
Partner API использует X-Partner-Key и предназначен для server-to-server
интеграций, когда вы онбордите сторонних мерчантов в ApiPay от своего имени.
Server-to-server API для партнёров-интеграторов (CRM, маркетплейсы, white-label-кассы).
Партнёр онбордит своих мерчантов, авторизует их Kaspi-кассы и выдаёт им платёжный ключ
X-API-Key — всё программно, из своей системы.
Платёжные операции, webhook-события и проверка HMAC живут в документации для мерчантов (/docs) и здесь не дублируются. Привязку кассира перед боевым подключением можно прогнать в песочнице партнёра — детерминированно, без единого реального вызова Kaspi и без SMS.
Если вы разрабатываете CRM-систему, платёжную платформу, агрегатор или white-label-кассу
и хотите подключать своих клиентов к приёму Kaspi Pay-платежей — Partner API ApiPay создан
именно для этого. Вы получаете единый X-Partner-Key → создаёте организацию
мерчанта → авторизуете кассира через Kaspi-SMS → выпускаете её X-API-Key.
Дальше мерчант создаёт счета по документации для мерчантов /docs.
| Поверхность | Базовый URL |
|---|---|
| Partner API (онбординг, выдача X-API-Key) | https://api.apipay.kz/api/partner |
| Платёжный API мерчанта (счета, lookup, webhooks) — см. /docs | https://api.apipay.kz/api/v1 |
Релевантны API ровно два ключа.
| Ключ / заголовок | Кто владеет | Для чего |
|---|---|---|
X-Partner-Key |
партнёр | server-to-server: создание организаций мерчантов, авторизация кассира, мониторинг своих организаций, выдача X-API-Key мерчанту. Берётся в партнёрском кабинете. |
X-API-Key |
конкретный мерчант (вы выдаёте ключ через Partner API) | платёжные операции от имени мерчанта (счета, статусы, возвраты, lookup) — описаны в документации для мерчантов /docs, здесь не дублируются. |
X-Partner-Key выдаётся в партнёрском кабинете; ротация ключа и переключение
sandbox/production — тоже в кабинете (фронт), не через API.
X-Partner-Key)
Префикс /api/partner. Лимит группы — 120 req/min на партнёра.
Server-to-server: создавайте организации мерчантов, авторизуйте кассира, мониторьте свои
организации и выдавайте X-API-Key мерчанту.
/api/partner/organizations — создать организацию мерчантаИдемпотентно по external_id — повторный POST вернёт существующую org.
Лимит 10/min. Ответ 201 (создана) / 200 (идемпотентный повтор).
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
has_catalog | boolean | нет | Создать организацию с каталогом товаров |
external_id | string | нет | Ваш идентификатор клиента в CRM (ключ идемпотентности) |
curl -X POST https://api.apipay.kz/api/partner/organizations \
-H 'X-Partner-Key: <X-Partner-Key>' -H 'Content-Type: application/json' \
-d '{"has_catalog":false,"external_id":"crm-client-42"}'
{ "success": true, "organization": {
"id": 50, "name": "ТОО Example", "idn": "123456789012",
"status": "verified", "sandbox_mode": true, "has_catalog": false,
"kaspi_connected": true, "session_mode": "self", "external_id": "crm-client-42",
"origin": "created", "payment_status": "active", "has_active_payment": true,
"created_at": "2026-05-16T10:00:00+05:00" } }
Опциональное поле name (max 255): пусто → автоген PARTNER_<id>_<ts>, позже подменяется именем из Kaspi на verify-otp.
/api/partner/organizations — список организаций (мониторинг)Мониторинг своих организаций: список с пагинацией. В карточке есть origin.
Query: per_page (1–100, def 25), page (def 1),
status (pending|verified|suspended), sandbox_mode (true/false/1/0),
is_test (true/false/1/0),
kyc_status (required|submitted|needs_changes|approved|blocked),
search (LIKE-поиск по name / external_id / idn, max 255).
Фильтры применяются до пагинации. Ключ data — алиас organizations (back-compat).
kyc_status принимает один статус или список через запятую:
?kyc_status=required,needs_changes — это клиенты, которым анкету нужно заполнить
или переделать. Неизвестное значение и пустая строка отбиваются 422, молча они не
игнорируются. is_test — не синоним sandbox_mode: у боевой организации
sandbox_mode остаётся true до первой успешной авторизации кассира.
{ "success": true, "organizations": ["<card>"], "data": ["<card>"],
"current_page": 1, "per_page": 25, "total": 42, "last_page": 2 }
/api/partner/organizations/{id} — карточка организацииПолучить карточку конкретной организации. → { "success": true, "organization": <card> }
/api/partner/organizations/{id} — отвязать организациюДеактивирует все API-ключи org и делает soft-delete. → { "success": true }
X-API-Key
перестаёт работать, а возвраты по ранее оплаченным счетам через ApiPay провести уже нельзя —
их делают вручную в приложении Kaspi Pay. Завершите нужные возвраты до вызова
DELETE. Отказ приходит не сразу: запрос на возврат принимается, но возврат
завершается статусом failed — вебхук invoice.refunded со
status: failed.
/api/partner/organizations/{id}/api-key — выдать X-API-Key мерчанту
Создать/перегенерировать ключ мерчанта + webhook. webhook_url проходит
SSRF-валидацию (приватные IP → 422). Повторный вызов НЕ безопасен для ретрая: он перевыпускает ключ — ранее выданные X-API-Key и webhook_secret перестают действовать сразу, и работающая интеграция мерчанта начнёт получать 401. Повторяйте только при плановой ротации.
Дальше мерчант работает этим ключом по документации для мерчантов
(/docs).
На боевой организации webhook_url должен быть адресом на домене: IP-адрес
или временный туннель (ngrok и подобные) отклоняются с 422. В ответе приходит
webhook_review_status — пока он не approved, события на этот
адрес мерчанту не доставляются.
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
name | string | нет | Произвольное название ключа |
webhook_url | string | да | URL для webhook-уведомлений мерчанта |
webhook_secret | string | нет | Секрет подписи (генерируется, если не указан) |
curl -X POST https://api.apipay.kz/api/partner/organizations/501/api-key \
-H 'X-Partner-Key: <X-Partner-Key>' -H 'Content-Type: application/json' \
-d '{"name":"CRM key","webhook_url":"https://crm.example.kz/sub/501/webhook"}'
{ "success": true, "key": "<X-API-Key, один раз>", "key_id": 900,
"webhook_url": "https://crm.example.kz/sub/501/webhook",
"webhook_secret": "whsec_yyyy", "is_org_default": true, "regenerated": false }
Префикс /api/partner/organizations/{id}/kaspi-auth. process_id
живёт 10 минут — шаги send-phone и verify-otp нужно выполнить в
этом окне. Боевая org доступна только production-партнёру (иначе
403 production_access_required); тестовая org всегда идёт мок-путём.
kaspi-auth/init → send-phone → verify-otp).
Предупредите клиента до онбординга и просите отдельный номер для роли «Кассир».
Обрыв виден в мониторинге: kaspi_connected: false в карточке организации и
счётчик needs_reauth в GET /api/partner/health.
.../kaspi-auth/init — Шаг 1Инициировать авторизацию кассира Kaspi. Body {} (опц. { "force": true }).
force: true — переавторизация поверх активной сессии (смена кассира). Без
force по уже подключённой org → 409 already_connected.
{ "success": true, "process_id": "SANDBOX-<uuid>", "process_status": "phone_required" }
.../kaspi-auth/send-phone — Шаг 2Body { "cashier_phone": "7XXXXXXXXXX" }. Kaspi отправляет SMS на номер кассира.
Успех → { "success": true, "process_status": "otp_required" }.
Ошибки: invalid_phone (422 — неверный формат), not_cashier
(422 — номер не «чистый кассир»), not_registered (422 — не зарегистрирован кассиром в Kaspi),
no_process (409 — нет активного процесса: вызовите init / он истёк),
context_expired (409 — Kaspi-контекст протух, ~10 мин: init заново + повтор send-phone),
cashier_unavailable (409 — кассира сейчас нельзя подключить; причина не раскрывается,
повтор не поможет — направьте мерчанта в поддержку ApiPay),
rate_limited (429 — за сутки с аккаунта пробовали слишком много разных номеров кассиров;
окно суточное, выждите время из Retry-After / retry_after_seconds; уже подключённые
кассиры этого владельца в счётчик не входят, переавторизация рабочей точки лимитом не блокируется),
sms_failed (502), kaspi_busy (503 — Kaspi недоступен; повторите примерно через минуту, окна в ответе нет).
В sandbox not_cashier / sms_failed / not_registered /
context_expired / kaspi_busy эмулируются магическими номерами (см. раздел Sandbox);
cashier_unavailable и rate_limited в песочнице не воспроизводятся:
тестовая организация идёт мок-путём. Заложите обработку заранее — на 409 нейтральное
сообщение мерчанту и отправка в поддержку, на 429 пауза по Retry-After.
.../kaspi-auth/verify-otp — Шаг 3Body { "otp": "0000" } (4–6 цифр). Подтвердить код из SMS.
200: { "success": true, "mode": "self", "organization": <card>, "process_status": "active" } — org → status: verified.200: { "success": false, "error": "invalid_otp", "process_status": "otp_required" } — повторяемо, сессия жива.cashier_unavailable (409): кассира сейчас нельзя подключить; причина не раскрывается, повтор не поможет — направьте мерчанта в поддержку ApiPay.0000 = успех, любой другой = invalid_otp..../kaspi-auth/status — статусТекущий статус авторизации кассира. status: none | pending | active | expired.
process_status: idle | phone_required | otp_required | active | failed (выводится из persisted-сессии).
{ "success": true, "status": "active", "process_status": "active",
"kaspi_connected": true, "expires_at": "2026-06-16T00:00:00+05:00" }
<card>| Поле | Тип | Описание |
|---|---|---|
id | number | ID организации |
name | string | Название |
idn | string | БИН/ИИН |
status | string | pending | verified | suspended |
sandbox_mode | boolean | Кассир Kaspi ещё не подключён. У боевой организации остаётся true до первой успешной авторизации кассира |
is_test | boolean | Организация создана, пока аккаунт был в режиме sandbox. Неизменяемо: такие организации живут по мок-контуру и удаляются при переводе аккаунта в production |
kyc_status | string | required | submitted | needs_changes | approved | blocked — статус анкеты клиента. Замечание модератора сюда не кладётся, оно только в GET .../kyc |
has_catalog | boolean | Есть каталог |
kaspi_connected | boolean | Касса Kaspi подключена |
session_mode | string | Режим привязки (self) |
external_id | string | Ваш CRM-идентификатор |
origin | string | referral | created | claimed — происхождение орги (никогда не null) |
payment_status | string | none | active | expired |
payment_expires_at | string|null | Когда истекает тариф |
has_active_payment | boolean | Активная оплата |
tariff | object | Действующие тарифные условия: tier, tier_label, daily_limit, is_custom, limits_source |
created_at | string | Дата создания |
{
"id": 50, "name": "ТОО Example", "idn": "123456789012",
"status": "pending|verified|suspended",
"sandbox_mode": false, "is_test": false,
"kyc_status": "required|submitted|needs_changes|approved|blocked",
"has_catalog": false, "kaspi_connected": true,
"session_mode": "self", "external_id": "crm-client-42",
"origin": "referral|created|claimed",
"payment_status": "none|active|expired",
"payment_expires_at": "2026-06-16T00:00:00+05:00",
"has_active_payment": false,
"tariff": { "tier": "business", "tier_label": "Бизнес", "daily_limit": 100,
"is_custom": false, "limits_source": "config|partner_grid|org_override" },
"created_at": "2026-05-16T10:00:00+05:00"
}
Про tariff. Это то, по каким условиям мерчант работает прямо
сейчас: tier — базовый идентификатор тарифа либо null (значением
custom он не бывает никогда), tier_label — отображаемое имя условий
(у договорных — их собственное, равенства с tier ждать не нужно),
daily_limit — суточный потолок счетов, is_custom — условия
договорные, а не из общей сетки, limits_source (config — общие
условия, partner_grid — ваша сетка, org_override — льгота этого
клиента). Сумм здесь нет: цена живёт в GET /organizations/{id}/tariff.
Партнёр сам платит ApiPay за подписку подключённого мерчанта
(start/business/pro/pro_max) и мониторит её. Оплата —
счётом через Kaspi: телефон плательщика в теле, тариф активируется
асинхронно после оплаты (webhook invoice.status_changed → paid).
Это подписочная плата мерчант→ApiPay, не оборот мерчанта.
/api/partner/tariff-plans — каталог тарифовОбщий каталог (без привязки к орг): 4 тарифа + 16 планов (4 × 1/3/6/12 мес).
→ { "success": true, "tiers": [...], "plans": [...] }
/api/partner/organizations/{id}/tariff — статус подписки мерчантаСнимок подписки. Нет тарифа → status: "none" (не 404). next_payment.amount — всегда.
{ "success": true, "tariff": {
"status": "none|trial|active|expired", "tier": "business", "is_trial": false,
"started_at": "2026-06-20T10:00:00+05:00", "expires_at": "2026-09-20T10:00:00+05:00",
"days_remaining": 92, "auto_renew": false,
"last_payment": { "amount": 71250, "paid_at": "...", "period_months": 3, "tier": "business", "status": "completed" },
"next_payment": { "due_at": "2026-09-20T10:00:00+05:00", "amount": 71250, "tier": "business" } } }
/api/partner/organizations/{id}/tariff/pay — оплатить тарифBody: { "tier_id": "business", "period_months": 3, "phone": "8XXXXXXXXXX", "set_billing_phone": false }
(phone — плательщик, формат 8XXXXXXXXXX; НЕ кассир). Боевая org — только
production-партнёру (иначе 403 production_access_required); тестовая →
мгновенная мок-активация (201, status:"completed", self_api_invoice_id:null).
{ "success": true, "payment": {
"id": 42, "amount": 71250, "status": "pending", "tier": "business", "period_months": 3,
"self_api_invoice_id": 778899, "external_id": "partner-tariff-501-1716900000",
"paid_at": null, "expires_at": null, "created_at": "2026-06-20T10:00:00+05:00", "failure_reason": null } }
Ошибки: 403 production_access_required, 404 organization_not_found,
409 tariff_payment_pending (в теле — существующий payment), 422 invalid_tariff_plan,
429 tariff_payment_cooldown (+Retry-After, retry_after_seconds:1800),
503 tariff_payment_unavailable (+Retry-After, retry_after_seconds:30).
/api/partner/organizations/{id}/tariff/assign — назначить тариф без оплатыНазначение тарифа без оплаты со стороны мерчанта: счёт не выставляется, с организации
ничего не списывается (payment.amount: 0, status: "completed").
Ручка доступна только партнёрам, которым ApiPay включил режим выдачи assignment;
в режиме payment она отвечает 403 assignment_not_enabled. Боевая
org — только production-партнёру. Расчёты с ApiPay по назначенным тарифам
ведутся отдельно, вне API.
Body: { "tier_id": "business", "period_months": 3 }
{ "success": true, "already_assigned": false, "payment": {
"id": 44, "amount": 0, "status": "completed", "tier": "start",
"period_months": 3, "self_api_invoice_id": null, "external_id": null,
"paid_at": "2026-08-13T10:00:00+05:00", "expires_at": "2026-11-13T10:00:00+05:00",
"created_at": "2026-08-13T10:00:00+05:00", "failure_reason": null } }
201 — тариф назначен; 200 с already_assigned: true —
запрошенный период уже покрыт назначением того же тарифа, второго срока и второго платежа не
появляется (ретрай CRM безопасен). Назначение поверх активного тарифа не сжигает остаток:
новый срок считается от текущего expires_at, если он в будущем. Актуальные
tier и expires_at читайте из GET .../tariff, а не
считайте у себя.
Ошибки: 403 assignment_not_enabled (у вас включён режим оплаты) /
production_access_required / forbidden;
404 organization_not_found; 409 test_organization,
tariff_payment_pending (есть неоплаченный счёт тарифа),
custom_tariff_locked (у организации индивидуальные условия),
hard_limited_org (действует ограничение по лимиту счетов, снимается на стороне
ApiPay); 422 tier_not_in_partner_grid (тариф вне вашей сетки),
422 assignment_horizon_exceeded (суммарный срок дальше 12 месяцев).
tariff.activated на своё
назначение не приходит: это было бы эхо на ваш же запрос, результат виден в синхронном
ответе.
/api/partner/organizations/{id}/tariff/payments и /{paymentId}Список: { "success": true, "data": [ <payment> ] } (по убыванию даты, триальные amount=0 исключены).
Один: { "success": true, "payment": <payment> }; неизвестный paymentId → 404 tariff_payment_not_found.
<payment>: { id, amount, status, tier, period_months, self_api_invoice_id, external_id, paid_at, expires_at, created_at, failure_reason }.
/api/partner/health — health аккаунта партнёраАгрегат по всем вашим организациям. Блок account отдаётся всегда актуальным,
остальные агрегаты кэшируются на несколько секунд.
{ "success": true, "api": {"status":"ok"},
"account": {"mode":"production","type":"operating",
"api_access_status":"granted","tariff_billing_mode":"assignment",
"inbound_sync":{"enabled":true,"secret_configured":true,
"secret_hint":"••••a1b2","accepting":true}},
"organizations": {"total":12,"kaspi_connected":9,"needs_reauth":1,"tariff_active":7,"tariff_expired":2,"on_trial":3,
"kyc":{"required":2,"submitted":1,"needs_changes":0,"approved":9,"blocked":0}},
"webhooks": {"delivered_24h":340,"failed_24h":5,"success_rate":98.6},
"rate_limits": {"partner_api_per_min":120} }
account — состояние самого аккаунта: mode
(sandbox|production), type (referral|operating),
api_access_status (none|pending|granted|rejected; условие перехода в
production — granted) и tariff_billing_mode (payment —
тариф мерчанта оплачивается через .../tariff/pay или
.../tariff/invoice; assignment — вы назначаете его без оплаты через
.../tariff/assign).
account.inbound_sync — состояние приёма входящих синхронизаций подписки:
enabled (тумблер), secret_configured (секрет задан и читается),
secret_hint (последние 4 символа под маской либо null),
accepting (дверь открыта).
accepting — из
остальных полей его не собрать. Смотрите сюда, а не в ответ боевого запроса: при закрытой
двери он отдаёт 401/403, и отличить «не тот секрет» от «приём
выключен» по нему нельзя. Сам секрет наружу не отдаётся никогда — только
secret_hint.
organizations.kyc — раскладка клиентов по статусу анкеты. Кто именно —
фильтром GET /api/partner/organizations?kyc_status=….
До одобрения анкеты у мерчанта действует суточный потолок счетов, поэтому статус анкеты нужен вам для сопровождения клиента. Партнёру доступны два действия: посмотреть статус и выдать клиенту персональную ссылку на анкету.
/api/partner/organizations/{id}/kyc — статус анкеты{ "success": true, "kyc": {
"status": "needs_changes", "required_action": "fix_and_resubmit",
"can_submit": true, "comment": "Скриншот витрины нечитаемый",
"submitted_at": "2026-08-14T12:00:00+05:00", "submitted_via": "invite" } }
| Поле | Значения | Описание |
|---|---|---|
status | required | submitted | needs_changes | approved | blocked | Статус анкеты клиента |
required_action | submit_profile | wait_review | fix_and_resubmit | none | contact_support | Что должно произойти дальше. Производное поле — читайте его вместо собственного маппинга наших статусов |
can_submit | boolean | Анкету сейчас можно подать или переподать |
comment | string|null | Замечание модератора. Приходит только при needs_changes — передайте клиенту дословно |
submitted_at | string|null | Когда анкета была подана |
submitted_via | merchant | invite | null | Из кабинета мерчанта либо по выданной вами ссылке |
Поллить каждую организацию не нужно. Сколько клиентов в каком состоянии — в
GET /api/partner/health (organizations.kyc); кто именно — фильтром
GET /api/partner/organizations?kyc_status=required,needs_changes. Статус каждой
организации есть и в её карточке — поле kyc_status.
/api/partner/organizations/{id}/kyc/invite — выдать клиенту ссылку на анкету{ "success": true, "invite_url": "https://apipay.kz/invite/2f6c…",
"expires_at": "2026-08-29T12:00:00+05:00" }
Передайте ссылку мерчанту — он заполнит и подтвердит анкету сам, аккаунт в ApiPay ему для этого не нужен.
Передавайте адрес ровно так, как он пришёл в invite_url, — не собирайте его у
себя из токена. Тем же механизмом мерчанту выдаётся и ссылка на подключение кассира, а адреса
у ссылок разного назначения не обязаны совпадать.
Отказы: 409 test_organization (тестовой организации ссылка не выдаётся),
409 kyc_already_submitted (анкета уже подана или проверена — смотрите её статус в
GET .../kyc), 403 forbidden,
404 organization_not_found, 429 — превышена частота выдачи ссылок.
kyc.status_changed
Решение по анкете приходит на ваш webhook_url — поллить статус каждой организации
не нужно. Payload плоский:
{ "event": "kyc.status_changed", "scope": "partner", "partner_id": 7,
"organization_id": 501, "external_id": "crm-client-42",
"previous_status": "submitted", "kyc_status": "needs_changes",
"comment": "Скриншот витрины нечитаемый",
"source": "Partner Key", "is_sandbox": false, "timestamp": "2026-08-15T12:00:00+05:00" }
Подпись — та же, что у tariff.activated:
X-Webhook-Signature: sha256=<HMAC-SHA256(body, webhook_secret)>. Событие
описывает переход, а не текущее состояние: оба статуса зафиксированы в момент
решения, поэтому повторная доставка не догоняет более позднее — актуальный статус всегда
читайте в GET .../kyc. comment приходит только при
needs_changes. Событие приходит только по организациям, которыми вы управляете;
мерчант, пришедший по вашей реферальной ссылке, ведёт анкету сам.
Для интеграторов, которые сами продают подписку своим клиентам и рассчитываются с ApiPay по договору. Вы объявляете текущее состояние подписки мерчанта — мы приводим его тариф в соответствие. Форма декларативная: повтор того же запроса безопасен, пропущенная доставка чинится следующей, порядок доставки значения не имеет.
/api/partner/organizations/{id}/subscriptionЗаголовки: X-Partner-Key, X-ApiPay-Timestamp (unix-секунды) и
X-ApiPay-Signature: sha256=<подпись>. Метка времени должна быть свежей:
расхождение в несколько минут отклоняется.
Схема подписи — строка из четырёх частей через точку:
{timestamp}.{МЕТОД}.{путь}.{сырое тело}, например
1786000000.PUT./api/partner/organizations/829/subscription.{"state":"active",…}.
Метод и путь входят в подпись намеренно: организация приезжает в пути, и подпись по одному телу
позволила бы переиграть перехваченный запрос на другую вашу организацию, не изменив ни байта
тела.
GET /api/partner/health →
account.inbound_sync; там же secret_hint — последние 4 символа,
чтобы сверить, тем ли секретом подписывает ваше окружение.
{ "state": "active", "tier": "business", "paid_through": "2026-09-12T18:50:12+05:00",
"cause": "autoprolongation", "trial": false,
"external_tariff_id": "23ca69d4-2657-40c4-8ba1-6ce24ddeac2e", "partner_context": {} }
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
state | string | да | active | suspended | cancelled — состояние подписки на вашей стороне |
tier | string | при active | Идентификатор тарифа ApiPay |
paid_through | string | при active | Дата, до которой подписка оплачена |
cause | string | нет | Повод изменения на вашей стороне — только для журнала |
trial | boolean | нет | Пробный период у вас. Информационный: наш собственный пробный период он не расходует |
external_tariff_id | string | нет | Ваш устойчивый идентификатор тарифа. Настоятельно рекомендуется: названия тарифов меняются, идентификаторы — нет |
partner_context | object | нет | Произвольный контекст для журнала. Персональных данных здесь быть не должно |
{ "success": true, "sync_id": 8123, "result": "applied",
"organization_id": 829, "external_id": "crm-client-42",
"applied": { "tier": "business", "tier_label": "Бизнес", "daily_limit": 100,
"limits_source": "partner_grid",
"expires_at": "2026-09-12T23:50:12+05:00", "status": "active" },
"ignored": { "daily_limit": { "sent": 999, "applied": 100 } } }
result: applied — тариф изменён; unchanged — уже
соответствует (в том числе если ваша дата не дальше текущей); noted —
приостановка или отмена зафиксирована, тариф не тронут. Блок applied — что реально
действует у мерчанта после синхронизации.
Инварианты — тариф двигается только вперёд и только вверх:
paid_through не укорачивает оплаченный срок → 200, result: unchanged;409 tier_downgrade_not_allowed) — понижает человек;422 assignment_horizon_exceeded);suspended и cancelled
фиксируются в журнале, срок не трогают, выданный период истечёт сам. Поэтому
синхронизируйте помесячно — тогда приостановка у вас становится приостановкой у нас
максимум через месяц.ignored рядом с тем, что
реально применено, чтобы расхождение было видно сразу, а не выяснялось из жалобы мерчанта на
лимит.
Скоуп организаций здесь шире, чем у остальных машинных ручек: доступны и созданные вами, и
заклеймленные. Исключение — заклеймленная организация, которая оплачивает тариф самостоятельно:
409 claimed_paying_organization. Такой перевод оформляет ApiPay, потому что
владение при claim не менялось.
SECRET='<секрет приёма>'
KEY='<X-Partner-Key>'
ORG=829
API_PATH="/api/partner/organizations/$ORG/subscription"
BODY='{"state":"active","tier":"business","paid_through":"2026-09-12T18:50:12+05:00","cause":"autoprolongation"}'
TS=$(date +%s)
SIG=$(printf '%s.PUT.%s.%s' "$TS" "$API_PATH" "$BODY" \
| openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)
curl -X PUT "https://api.apipay.kz$API_PATH" \
-H "X-Partner-Key: $KEY" \
-H "X-ApiPay-Timestamp: $TS" \
-H "X-ApiPay-Signature: sha256=$SIG" \
-H 'Content-Type: application/json' \
--data-raw "$BODY"
--data-raw "$BODY" — не
--data @file с последующим форматированием и не пересборка JSON библиотекой.
Подпись считается по сырым байтам: одна лишняя пробельная позиция —
401 invalid_signature. Держите ту же строку, которую подписали.
| HTTP | error_code | Что значит |
|---|---|---|
| 200 | — | result: applied / unchanged / noted |
| 401 | invalid_signature, signature_expired, inbound_not_configured | подпись, свежесть метки, приём не настроен |
| 403 | inbound_sync_disabled, assignment_not_enabled, partner_not_active | приём выключен / режим оплаты / аккаунт неактивен |
| 404 | organization_not_found | организация не ваша либо не существует |
| 409 | test_organization, organization_deleted, claimed_paying_organization, custom_tariff_locked, hard_limited_org, tier_downgrade_not_allowed, tariff_payment_pending | условия организации |
| 413 | payload_too_large | тело больше допустимого |
| 422 | unsupported_state, subscription_terms_required, invalid_paid_through, tier_not_in_partner_grid, assignment_horizon_exceeded | форма или условия |
| 429 | rate_limited, tier_switch_rate_limited | частота вызовов (у этой ручки собственный бакет) / слишком частая смена тарифа этой организации |
/api/partner/subscription-syncs и /{sync}Журнал: что мы приняли и почему отказали — по каждому вашему вызову, включая отказы.
Карточка одной записи дополнительно отдаёт исходное тело запроса (payload); в
списке его нет.
Query: organization_id, result
(applied|unchanged|noted|rejected|failed), state
(active|suspended|cancelled), окно from/to по времени
приёма (голая дата трактуется в Asia/Almaty; to не раньше from, иначе
422), per_page (1–100, def 25), page (def 1).
Неизвестное значение фильтра — 422, а не пустая страница: молчаливый ноль
неотличим от «вы нам ничего не присылали». Чужой organization_id, наоборот, даёт
пустую страницу — существование чужих записей мы не подтверждаем.
{ "success": true, "data": [
{ "id": 8123, "organization_id": 829, "state": "active", "cause": "autoprolongation",
"requested_tier": "business", "requested_paid_through": "2026-09-12T18:50:12+05:00",
"applied_tier": "business", "applied_expires_at": "2026-09-12T23:50:12+05:00",
"result": "applied", "outcome_code": "applied", "http_status": 200,
"external_tariff_id": "23ca69d4-2657-40c4-8ba1-6ce24ddeac2e",
"received_at": "2026-08-12T18:50:13+05:00" } ],
"current_page": 1, "per_page": 25, "total": 1, "last_page": 1 }
outcome_code — точная причина отказа или успеха: слаг ошибки либо
applied/unchanged/noted. Неизвестная или чужая запись в
карточке → 404 sync_not_found.
curl "https://api.apipay.kz/api/partner/subscription-syncs?result=rejected&from=2026-08-01" \
-H "X-Partner-Key: $KEY"
{ "success": false, "error": "<code>", "error_code": "<code>", "message": "<ru>", "errors"?: {...} }
(error дублирует error_code для back-compat; errors — только на 422 из контроллера).
Middleware (аутентификация/владение/production-гейт): сокращённая форма { "success": false, "error": "<code>" }.
Валидация FormRequest: стандартная Laravel-форма { "message", "errors" } без success/error_code.
Коды error_code стабильны — используйте для локализации.+05:00). Исключение: timestamp внутри webhook-payload — UTC (+00:00).{ "message": "Too Many Attempts." } + Retry-After. Суточный лимит подключения кассира на send-phone — error_code: "rate_limited" + retry_after_seconds; различайте по наличию error_code, иначе суточная пауза применится к обычному поминутному throttle.
Тарифные лимиты (tariff/pay) кладут в тело retry_after_seconds (1800/30).
Песочница партнёра детерминированно эмулирует привязку кассира: ни одного
реального вызова Kaspi, ни одной SMS. Магические номера на send-phone подменяют
ответ Kaspi на фиксированный; любой другой валидный номер 7XXXXXXXXXX трактуется
как «обычный» (success). Тестовая организация архитектурно не может стать боевой.
simulate-status) — это песочница мерчанта,
отдельная система: см. документацию для мерчантов /docs.
Магические значения (только привязка кассира):
| Шаг / магическое значение | Результат | HTTP |
|---|---|---|
kaspi-auth/init | process_id = "SANDBOX-<uuid>" | 200 |
send-phone 77770000010 | success (касса привязывается) | 200 |
send-phone 77770000011 | not_cashier — номер не кассир Kaspi | 422 |
send-phone 77770000012 | sms_failed — Kaspi не смог отправить SMS | 502 |
send-phone 77770000013 | not_registered — номер не зарегистрирован кассиром в Kaspi | 422 |
send-phone 77770000014 | context_expired — Kaspi-контекст протух (повторите init) | 409 |
send-phone 77770000015 | kaspi_busy — Kaspi временно недоступен | 503 |
send-phone прочий валидный 7… | success | 200 |
verify-otp 0000 | success → org status: verified (sandbox_mode остаётся true) | 200 |
verify-otp любой другой код | invalid_otp (повторяемо, сессия жива) | 200 |
POST /organizations {"external_id":"test-1"} → org.idPOST .../{id}/kaspi-auth/init → process_idPOST .../send-phone {"cashier_phone":"77770000010"} → successPOST .../verify-otp {"otp":"0000"} → org status: verifiedPOST .../{id}/api-key → X-API-Key мерчанта (дальше — по /docs)POST .../send-phone {"cashier_phone":"77770000011"} → 422 not_cashierPOST .../send-phone {"cashier_phone":"77770000012"} → 502 sms_failedPOST .../send-phone {"cashier_phone":"77770000013"} → 422 not_registeredPOST .../send-phone {"cashier_phone":"77770000014"} → 409 context_expiredinit заново и повторите send-phone (один раз).POST .../send-phone {"cashier_phone":"77770000015"} → 503 kaspi_busyRetry-After секунд, покажите «Kaspi временно недоступен».POST .../send-phone {"cashier_phone":"77770000010"} → successPOST .../verify-otp {"otp":"1234"} → { "success": false, "error": "invalid_otp" } — повторяемо0000 — сессия остаётся живой.| Лимит | Значение | Превышение |
|---|---|---|
| Тестовых организаций на партнёра | 20 | 429 test_org_limit |
| Новые номера кассиров на владельца | суточное окно | 429 rate_limited (Retry-After + retry_after_seconds) |
| Создание организаций | 10 req/min | throttle |
| kaspi-auth (тестовая org) | 60 req/min | throttle (на партнёра+org) |
| kaspi-auth (боевая org) | 10 req/min | throttle (на партнёра+org) |
| Вся группа Partner API | 120 req/min | throttle (на партнёра) |
Партнёрский онбординг до выдачи X-API-Key:
X-Partner-Key в партнёрском кабинете (фронт).POST /api/partner/organizations {external_id} → org.idPOST .../{id}/kaspi-auth/init → process_idPOST .../{id}/kaspi-auth/send-phone {"cashier_phone":"77770000010"} → successPOST .../{id}/kaspi-auth/verify-otp {"otp":"0000"} → org status: verifiedPOST .../{id}/api-key → X-API-Key мерчанта + webhook_secretX-API-Key — по документации для мерчантов (/docs).GET /api/partner/organizations.
Партнёр задаёт webhook_url при выдаче X-API-Key. Формат событий и
проверка HMAC-подписи описаны в документации для мерчантов
(/docs) — здесь не дублируются.
X-Partner-Key выдаётся в партнёрском кабинете (фронт). Sandbox — сразу
self-service. Production (реальный Kaspi-auth и mode=production) — после ручного
одобрения админом; переключение режима тоже в кабинете.
Онбординг мерчанта и привязку кассира — детерминированно, без реального Kaspi и SMS (магические номера). Тестирование счетов/возвратов/webhooks — это отдельная песочница мерчанта, см. /docs.
В документации для мерчантов /docs. Партнёр задаёт
webhook_url при выдаче X-API-Key, а формат событий и проверку HMAC
мерчант (и ваш сервер) берёт из /docs.
Оформите партнёрский статус на сайте — sandbox-доступ и X-Partner-Key
выдаются в партнёрском кабинете. Прогоните привязку кассира без единого реального вызова
Kaspi, затем запросите production-доступ у админа.
Подать заявку на партнёрство →
Поддержка: WhatsApp +7 700 307 65 12