Два ключа
У клиента нет отдельного аккаунта ApiPay: организацию, ключи и вебхук держит партнёр. Единственное действие клиента — подтвердить код из Kaspi-SMS при авторизации кассира (код приходит на номер кассира клиента, а не партнёру).
Ключей два, не путайте их — это главный источник ошибок:
X-Partner-Key— партнёрский, server-to-server: онбординг, авторизация кассира, выдача ключей, тариф, health. Хост.../api/partner.- per-org
X-API-Key— ключ конкретного клиента: счета, статусы, возвраты, каталог, lookup. Хост.../api/v1.
Разбор пары ключей — в статье «Partner API white-label».
Шаг 1. Создать организацию клиента (авто)
POST /organizations с телом { "external_id": "crm-client-42", "name": "ТОО Клиент", "has_catalog": false }. external_id — ваш референс клиента в CRM и ключ идемпотентности: повторный вызов с тем же external_id вернёт ту же организацию (200), дубля не будет. В ответе — organization.id, status: "pending", sandbox_mode. Никакого участия клиента.
Шаг 2. Авторизация кассира — единственный ручной штрих
Три вызова подряд: POST /organizations/{id}/kaspi-auth/init → send-phone → verify-otp. На send-phone вы передаёте телефон кассира клиента в формате 7XXXXXXXXXX; Kaspi шлёт SMS-код на этот номер. Клиент диктует код — вы отправляете его в verify-otp ({ "otp": "1234" }). Всё, организация становится verified.
process_idживёт ~10 минут — уложитеsend-phone+verify-otpв это окно.- Слетела сессия позже — переавторизация тем же флоу с
init+"force": true.
Ветки отказа send-phone и verify-otp (в том числе 409 cashier_unavailable, суточный 429 rate_limited и терминальные исходы, после которых нужен новый init) разобраны в статье «Как партнёру подключить организацию» — заложите их обработку до запуска.
Шаг 3. Выдать клиенту ключ и вебхук (авто)
POST /organizations/{id}/api-key с обязательным webhook_url (проходит SSRF-валидацию — приватный адрес 422); указывайте постоянный домен своего сервиса, не шортенер и не временный адрес-перехватчик. В ответе key (это X-API-Key) и webhook_secret приходят в открытом виде ровно один раз — сохраните оба сразу в своё секрет-хранилище, привязав к записи клиента. Клиент этих ключей не видит и не хранит — они живут у партнёра. Как ключ соотносится с секретом подписи — «API-ключ и вебхук-секрет».
Шаг 4. Счета, каталог, статусы, возвраты (авто)
Дальше работаете выданным X-API-Key против https://api.apipay.kz/api/v1 — от имени клиента, но без его участия:
- Счёт по номеру —
POST /api/v1/invoices(201, обработка асинхронная). - QR-счёт на экране кассы —
POST /api/v1/invoices/qr. Срок жизни берите изqr_expires_at, а не из константы: окно и лимиты — в статье «QR-счёт: окно и лимиты». - Возврат —
POST /api/v1/invoices/{id}/refund(полный или частичный).
Полный платёжный API — в мерчантской документации /docs; создание счетов по номеру — «Счёт Kaspi по номеру».
Шаг 5. Оплата тарифа ApiPay — два способа
Партнёр сам платит ApiPay за подписку клиента (start/business/pro/pro_max — сетка в статье «Тарифы и комиссия»). Это подписочная плата клиент→ApiPay, не оборот клиента. Два независимых способа оплатить один тариф:
- По телефону —
POST /organizations/{id}/tariff/payс{ "tier_id", "period_months", "phone": "8XXXXXXXXXX" }. Push-счёт через Kaspi на телефон плательщика; активация асинхронная после оплаты. - Счётом на юрлицо —
POST /organizations/{id}/tariff/invoiceс реквизитами покупателя (buyer_bin12 цифр,buyer_name, опц.buyer_address/contract). Синхронно возвращаетdownload_url— публичную ссылку на PDF-счёт, который оплачивается банковским переводом. Тариф активируется вручную владельцем ApiPay после поступления средств — автоактивации у счёта нет.
Неоплаченный счёт (payment_method=invoice) не блокирует tariff/pay, и наоборот — способы независимы. Точную сумму за выбранный период возвращает сервер (GET /tariff-plans), не считайте её сами.
Шаг 6. Вебхуки закрывают петлю без поллинга
Два события снимают необходимость постоянно опрашивать статусы:
invoice.status_changed— приходит на per-orgwebhook_urlклиента, когда счёт меняет статус (в т.ч.paid— клиент оплатил). Реагируйте на последний статус: легитимны иcancelled → paid, иexpired → paid.tariff.activated— приходит наwebhook_urlпартнёра, когда владелец ApiPay вручную активировал тариф по выписанному счёту. Плоский payload сpayment_id,invoice_number,tier,amount,expires_at.
Обе подписи — X-Webhook-Signature: sha256=HMAC-SHA256(raw body, secret), проверяйте по сырому телу, до JSON-парсинга (готовые приёмники — «Настройка вебхуков ApiPay»). Секреты разные: invoice.status_changed подписан per-org webhook_secret организации клиента, а tariff.activated — партнёрским webhook_secret, который выдан вместе с X-Partner-Key. Доставка tariff.activated повторяется автоматически (до 11 попыток); при окончательном сбое активацию видно поллингом GET .../tariff/payments (pending → completed).
Сначала — sandbox
Весь пайплайн прогоняется в песочнице партнёра без реального Kaspi и SMS, детерминированно (магические номера кассира, OTP 0000) — можно покрыть автотестами. Пока партнёр в sandbox, боевые вызовы отбиваются 403 production_access_required; production открывается после ручного одобрения.
Не путайте телефоны: кассир — 7XXXXXXXXXX (авторизация Kaspi), плательщик — 8XXXXXXXXXX (кому выставляете счёт).
Частые вопросы
Заходит ли клиент в кабинет ApiPay?
Нет, ни разу. У клиента нет отдельного аккаунта ApiPay: организацию, X-API-Key и вебхук держит партнёр. Единственное действие клиента — продиктовать код из Kaspi-SMS при авторизации кассира.
Как узнать, что тариф по счёту активирован?
Придёт вебхук tariff.activated на webhook_url партнёра. Если он не дошёл (после ретраев), тот же факт виден поллингом GET .../tariff/payments: статус платежа payment_method=invoice переходит pending → completed. Автоактивации у tariff/invoice нет.
Куда приходят вебхуки об оплате счетов клиента?
На per-org webhook_url клиента (ключа, создавшего счёт). Партнёрский webhook_url в доставке invoice/refund не участвует — на него идёт только tariff.activated.