Это Partner API ApiPay — для платформ и CRM, подключающих СВОИХ клиентов.

Если вы мерчант и хотите принимать платежи через Kaspi Pay в своём бизнесе — вам нужна обычная документация:

Partner API использует X-Partner-Key и предназначен для server-to-server интеграций, когда вы онбордите сторонних мерчантов в ApiPay от своего имени.

Partner API ApiPay

Server-to-server API для партнёров-интеграторов (CRM, маркетплейсы, white-label-кассы). Партнёр онбордит своих мерчантов, авторизует их Kaspi-кассы и выдаёт им платёжный ключ X-API-Key — всё программно, из своей системы.

Платёжные операции, webhook-события и проверка HMAC живут в документации для мерчантов (/docs) и здесь не дублируются. Привязку кассира перед боевым подключением можно прогнать в песочнице партнёра — детерминированно, без единого реального вызова Kaspi и без SMS.

Для разработчиков CRM и платформ

Если вы разрабатываете CRM-систему, платёжную платформу, агрегатор или white-label-кассу и хотите подключать своих клиентов к приёму Kaspi Pay-платежей — Partner API ApiPay создан именно для этого. Вы получаете единый X-Partner-Key → создаёте организацию мерчанта → авторизуете кассира через Kaspi-SMS → выпускаете её X-API-Key. Дальше мерчант создаёт счета по документации для мерчантов /docs.

Оформить партнёрский статус →

1. Базовые URL

ПоверхностьБазовый URL
Partner API (онбординг, выдача X-API-Key)https://api.apipay.kz/api/partner
Платёжный API мерчанта (счета, lookup, webhooks) — см. /docshttps://api.apipay.kz/api/v1

2. Ключи аутентификации

Релевантны API ровно два ключа.

Ключ / заголовокКто владеетДля чего
X-Partner-Key партнёр server-to-server: создание организаций мерчантов, авторизация кассира, мониторинг своих организаций, выдача X-API-Key мерчанту. Берётся в партнёрском кабинете.
X-API-Key конкретный мерчант (вы выдаёте ключ через Partner API) платёжные операции от имени мерчанта (счета, статусы, возвраты, lookup) — описаны в документации для мерчантов /docs, здесь не дублируются.

X-Partner-Key выдаётся в партнёрском кабинете; ротация ключа и переключение sandbox/production — тоже в кабинете (фронт), не через API.

3. Partner API (X-Partner-Key)

Префикс /api/partner. Лимит группы — 120 req/min на партнёра. Server-to-server: создавайте организации мерчантов, авторизуйте кассира, мониторьте свои организации и выдавайте X-API-Key мерчанту.

POST /api/partner/organizations — создать организацию мерчанта

Идемпотентно по external_id — повторный POST вернёт существующую org. Лимит 10/min. Ответ 201 (создана) / 200 (идемпотентный повтор).

ПолеТипОбязательноеОписание
has_catalogbooleanнетСоздать организацию с каталогом товаров
external_idstringнетВаш идентификатор клиента в 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.

GET /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 }

GET /api/partner/organizations/{id} — карточка организации

Получить карточку конкретной организации. → { "success": true, "organization": <card> }

DELETE /api/partner/organizations/{id} — отвязать организацию

Деактивирует все API-ключи org и делает soft-delete. → { "success": true }

Отвязка сразу останавливает интеграцию мерчанта. Его X-API-Key перестаёт работать, а возвраты по ранее оплаченным счетам через ApiPay провести уже нельзя — их делают вручную в приложении Kaspi Pay. Завершите нужные возвраты до вызова DELETE. Отказ приходит не сразу: запрос на возврат принимается, но возврат завершается статусом failed — вебхук invoice.refunded со status: failed.

POST /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, события на этот адрес мерчанту не доставляются.

ПолеТипОбязательноеОписание
namestringнетПроизвольное название ключа
webhook_urlstringдаURL для webhook-уведомлений мерчанта
webhook_secretstringнетСекрет подписи (генерируется, если не указан)
key и webhook_secret показываются ОДИН раз — сохраните их при получении.
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 }

Авторизация кассира Kaspi (3 шага)

Префикс /api/partner/organizations/{id}/kaspi-auth. process_id живёт 10 минут — шаги send-phone и verify-otp нужно выполнить в этом окне. Боевая org доступна только production-партнёру (иначе 403 production_access_required); тестовая org всегда идёт мок-путём.

Номер кассира — отдельный номер. У кассира в Kaspi Pay активна одна сессия. Если мерчант войдёт в Kaspi Pay под номером, который вы привязали, привязка завершится и счета перестанут создаваться — нужна повторная авторизация (kaspi-auth/initsend-phoneverify-otp). Предупредите клиента до онбординга и просите отдельный номер для роли «Кассир». Обрыв виден в мониторинге: kaspi_connected: false в карточке организации и счётчик needs_reauth в GET /api/partner/health.

POST .../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" }

POST .../kaspi-auth/send-phone — Шаг 2

Body { "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.

POST .../kaspi-auth/verify-otp — Шаг 3

Body { "otp": "0000" } (4–6 цифр). Подтвердить код из SMS.

GET .../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" }

4. Карточка организации <card>

ПолеТипОписание
idnumberID организации
namestringНазвание
idnstringБИН/ИИН
statusstringpending | verified | suspended
sandbox_modebooleanКассир Kaspi ещё не подключён. У боевой организации остаётся true до первой успешной авторизации кассира
is_testbooleanОрганизация создана, пока аккаунт был в режиме sandbox. Неизменяемо: такие организации живут по мок-контуру и удаляются при переводе аккаунта в production
kyc_statusstringrequired | submitted | needs_changes | approved | blocked — статус анкеты клиента. Замечание модератора сюда не кладётся, оно только в GET .../kyc
has_catalogbooleanЕсть каталог
kaspi_connectedbooleanКасса Kaspi подключена
session_modestringРежим привязки (self)
external_idstringВаш CRM-идентификатор
originstringreferral | created | claimed — происхождение орги (никогда не null)
payment_statusstringnone | active | expired
payment_expires_atstring|nullКогда истекает тариф
has_active_paymentbooleanАктивная оплата
tariffobjectДействующие тарифные условия: tier, tier_label, daily_limit, is_custom, limits_source
created_atstringДата создания
{
  "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.

5. Тариф и здоровье

Партнёр сам платит ApiPay за подписку подключённого мерчанта (start/business/pro/pro_max) и мониторит её. Оплата — счётом через Kaspi: телефон плательщика в теле, тариф активируется асинхронно после оплаты (webhook invoice.status_changedpaid). Это подписочная плата мерчант→ApiPay, не оборот мерчанта.

GET /api/partner/tariff-plans — каталог тарифов

Общий каталог (без привязки к орг): 4 тарифа + 16 планов (4 × 1/3/6/12 мес). → { "success": true, "tiers": [...], "plans": [...] }

GET /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" } } }

POST /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).

POST /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 месяцев).

Отзыва назначения в API нет. Если тариф назначен ошибочно — напишите нам, снимем со стороны ApiPay. Вебхук tariff.activated на своё назначение не приходит: это было бы эхо на ваш же запрос, результат виден в синхронном ответе.

GET /api/partner/organizations/{id}/tariff/payments и /{paymentId}

Список: { "success": true, "data": [ <payment> ] } (по убыванию даты, триальные amount=0 исключены). Один: { "success": true, "payment": <payment> }; неизвестный paymentId404 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 }.

GET /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=….

6. KYC мерчанта

До одобрения анкеты у мерчанта действует суточный потолок счетов, поэтому статус анкеты нужен вам для сопровождения клиента. Партнёру доступны два действия: посмотреть статус и выдать клиенту персональную ссылку на анкету.

Подать анкету за мерчанта нельзя, такой ручки нет. В анкете есть подтверждение о неторговле запрещённым — заверение, которое даёт тот, у кого факты. Анкета, заполненная партнёром, превращает видимый пробел в невидимое утверждение.

GET /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" } }
ПолеЗначенияОписание
statusrequired | submitted | needs_changes | approved | blockedСтатус анкеты клиента
required_actionsubmit_profile | wait_review | fix_and_resubmit | none | contact_supportЧто должно произойти дальше. Производное поле — читайте его вместо собственного маппинга наших статусов
can_submitbooleanАнкету сейчас можно подать или переподать
commentstring|nullЗамечание модератора. Приходит только при needs_changes — передайте клиенту дословно
submitted_atstring|nullКогда анкета была подана
submitted_viamerchant | invite | nullИз кабинета мерчанта либо по выданной вами ссылке

Поллить каждую организацию не нужно. Сколько клиентов в каком состоянии — в GET /api/partner/health (organizations.kyc); кто именно — фильтром GET /api/partner/organizations?kyc_status=required,needs_changes. Статус каждой организации есть и в её карточке — поле kyc_status.

POST /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. Событие приходит только по организациям, которыми вы управляете; мерчант, пришедший по вашей реферальной ссылке, ведёт анкету сам.

7. Синхронизация подписки (подписанный запрос)

Для интеграторов, которые сами продают подписку своим клиентам и рассчитываются с ApiPay по договору. Вы объявляете текущее состояние подписки мерчанта — мы приводим его тариф в соответствие. Форма декларативная: повтор того же запроса безопасен, пропущенная доставка чинится следующей, порядок доставки значения не имеет.

PUT /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",…}. Метод и путь входят в подпись намеренно: организация приезжает в пути, и подпись по одному телу позволила бы переиграть перехваченный запрос на другую вашу организацию, не изменив ни байта тела.

Секрет приёма входящей синхронизации — отдельный. Это не тот секрет, которым мы подписываем исходящие вебхуки. Выдаёт его ApiPay по вашему запросу, показывается он один раз, перевыдача сразу перестаёт принимать старую подпись — плановую смену согласовывайте заранее. Задан ли секрет и открыт ли приём, видно в GET /api/partner/healthaccount.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": {} }
ПолеТипОбязательноеОписание
statestringдаactive | suspended | cancelled — состояние подписки на вашей стороне
tierstringпри activeИдентификатор тарифа ApiPay
paid_throughstringпри activeДата, до которой подписка оплачена
causestringнетПовод изменения на вашей стороне — только для журнала
trialbooleanнетПробный период у вас. Информационный: наш собственный пробный период он не расходует
external_tariff_idstringнетВаш устойчивый идентификатор тарифа. Настоятельно рекомендуется: названия тарифов меняются, идентификаторы — нет
partner_contextobjectнетПроизвольный контекст для журнала. Персональных данных здесь быть не должно
{ "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 — что реально действует у мерчанта после синхронизации.

Инварианты — тариф двигается только вперёд и только вверх:

Суточный лимит счетов и название тарифа определяются вашими договорными условиями, а не запросом. Прислали своё — вернём в блоке 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. Держите ту же строку, которую подписали.
HTTPerror_codeЧто значит
200result: applied / unchanged / noted
401invalid_signature, signature_expired, inbound_not_configuredподпись, свежесть метки, приём не настроен
403inbound_sync_disabled, assignment_not_enabled, partner_not_activeприём выключен / режим оплаты / аккаунт неактивен
404organization_not_foundорганизация не ваша либо не существует
409test_organization, organization_deleted, claimed_paying_organization, custom_tariff_locked, hard_limited_org, tier_downgrade_not_allowed, tariff_payment_pendingусловия организации
413payload_too_largeтело больше допустимого
422unsupported_state, subscription_terms_required, invalid_paid_through, tier_not_in_partner_grid, assignment_horizon_exceededформа или условия
429rate_limited, tier_switch_rate_limitedчастота вызовов (у этой ручки собственный бакет) / слишком частая смена тарифа этой организации

GET /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.

Журнал хранится 90 дней, более старые записи подрезаются. Если он нужен вам дольше — забирайте страницы к себе: восстановить подрезанное мы не сможем.
curl "https://api.apipay.kz/api/partner/subscription-syncs?result=rejected&from=2026-08-01" \
  -H "X-Partner-Key: $KEY"

8. Ошибки и форматы (сквозное)

9. Sandbox — отладка привязки кассира

Песочница партнёра детерминированно эмулирует привязку кассира: ни одного реального вызова Kaspi, ни одной SMS. Магические номера на send-phone подменяют ответ Kaspi на фиксированный; любой другой валидный номер 7XXXXXXXXXX трактуется как «обычный» (success). Тестовая организация архитектурно не может стать боевой.

Не путайте песочницу партнёра и песочницу мерчанта. Здесь — только онбординг и привязка кассира. Тестирование счетов, возвратов и webhook-событий (например, через simulate-status) — это песочница мерчанта, отдельная система: см. документацию для мерчантов /docs.

Магические значения (только привязка кассира):

Шаг / магическое значениеРезультатHTTP
kaspi-auth/initprocess_id = "SANDBOX-<uuid>"200
send-phone 77770000010success (касса привязывается)200
send-phone 77770000011not_cashier — номер не кассир Kaspi422
send-phone 77770000012sms_failed — Kaspi не смог отправить SMS502
send-phone 77770000013not_registered — номер не зарегистрирован кассиром в Kaspi422
send-phone 77770000014context_expired — Kaspi-контекст протух (повторите init)409
send-phone 77770000015kaspi_busy — Kaspi временно недоступен503
send-phone прочий валидный 7…success200
verify-otp 0000success → org status: verified (sandbox_mode остаётся true)200
verify-otp любой другой кодinvalid_otp (повторяемо, сессия жива)200

Сценарии тестирования

Сценарий 1 — успешная привязка (happy path)

Сценарий 2 — номер не кассир

Сценарий 3 — сбой отправки SMS

Сценарий 3a — номер не зарегистрирован кассиром

Сценарий 3b — Kaspi-контекст протух

Сценарий 3c — Kaspi временно недоступен

Сценарий 4 — неверный код из SMS

10. Лимиты

ЛимитЗначениеПревышение
Тестовых организаций на партнёра20429 test_org_limit
Новые номера кассиров на владельцасуточное окно429 rate_limited (Retry-After + retry_after_seconds)
Создание организаций10 req/minthrottle
kaspi-auth (тестовая org)60 req/minthrottle (на партнёра+org)
kaspi-auth (боевая org)10 req/minthrottle (на партнёра+org)
Вся группа Partner API120 req/minthrottle (на партнёра)

11. Полный E2E (sandbox)

Партнёрский онбординг до выдачи X-API-Key:

  1. Получите X-Partner-Key в партнёрском кабинете (фронт).
  2. POST /api/partner/organizations {external_id}org.id
  3. POST .../{id}/kaspi-auth/initprocess_id
  4. POST .../{id}/kaspi-auth/send-phone {"cashier_phone":"77770000010"} → success
  5. POST .../{id}/kaspi-auth/verify-otp {"otp":"0000"} → org status: verified
  6. POST .../{id}/api-keyX-API-Key мерчанта + webhook_secret
  7. Мерчант создаёт счета этим X-API-Key — по документации для мерчантов (/docs).
  8. Мониторьте свои организации: GET /api/partner/organizations.

12. Webhooks и HMAC

Партнёр задаёт webhook_url при выдаче X-API-Key. Формат событий и проверка HMAC-подписи описаны в документации для мерчантов (/docs) — здесь не дублируются.

13. FAQ

Как получить доступ?

X-Partner-Key выдаётся в партнёрском кабинете (фронт). Sandbox — сразу self-service. Production (реальный Kaspi-auth и mode=production) — после ручного одобрения админом; переключение режима тоже в кабинете.

Что покрывает песочница партнёра?

Онбординг мерчанта и привязку кассира — детерминированно, без реального Kaspi и SMS (магические номера). Тестирование счетов/возвратов/webhooks — это отдельная песочница мерчанта, см. /docs.

Где платёжные операции и webhooks?

В документации для мерчантов /docs. Партнёр задаёт webhook_url при выдаче X-API-Key, а формат событий и проверку HMAC мерчант (и ваш сервер) берёт из /docs.


Готовы интегрировать ApiPay в вашу CRM или платформу?

Оформите партнёрский статус на сайте — sandbox-доступ и X-Partner-Key выдаются в партнёрском кабинете. Прогоните привязку кассира без единого реального вызова Kaspi, затем запросите production-доступ у админа.

Подать заявку на партнёрство →

Поддержка: WhatsApp +7 700 307 65 12

Связанные страницы

Документация REST APIПолный справочник эндпоинтов Интеграция с n8nАвтоматизация платежей без кода Kaspi Pay REST APIОбзор возможностей API приёма платежей Интеграция Kaspi PayПошаговое подключение к сайту