Что означает «полностью автоматически»
Полностью автоматически — это когда клиент партнёра пользуется вашим продуктом (CRM, платформа, SaaS, 1С), а приём Kaspi «просто работает» под вашим брендом. У клиента нет отдельного аккаунта 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в это окно.- Неверный код — это тоже HTTP 200 с
{ "success": false, "error": "invalid_otp" }, сессия не сбрасывается: просто попросите код заново и повторитеverify-otp. - Слетела сессия позже — переавторизация тем же флоу с
init+"force": true.
Пошаговый онбординг с разбором каждой ошибки send-phone — в статье «Как партнёру подключить организацию».
Шаг 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(TTL минуты). - Возврат —
POST /api/v1/invoices/{id}/refund(полный или частичный).
Полный платёжный API — в мерчантской документации /docs; создание счетов по номеру — «Счёт Kaspi по номеру».
Шаг 5. Оплата тарифа ApiPay — два способа
Партнёр сам платит ApiPay за подписку клиента (start 10 000 ₸/мес до 30 счетов/день, business 25 000 ₸ до 100, pro 60 000 ₸ 100–300; больше 300 — договорная). Это подписочная плата клиент→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, webhook_secret); проверяйте по сырому телу, до JSON-парсинга. Доставка tariff.activated ретраится автоматически (до 11 попыток); при окончательном сбое активацию видно поллингом GET .../tariff/payments (pending → completed). Готовые приёмники и проверка HMAC — «Настройка вебхуков ApiPay».
Сначала — sandbox
Весь пайплайн прогоняется в песочнице партнёра без реального Kaspi и SMS, детерминированно (магические номера кассира, OTP 0000) — можно покрыть автотестами. Тот же код идёт в production, отличаются только реальные значения. Пока партнёр в sandbox, боевые вызовы отбиваются 403 production_access_required; production открывается после ручного одобрения (коммерческий договор).
Частые ошибки
- Просить клиента зайти в кабинет ApiPay. Не нужно: у клиента нет аккаунта, всё делает партнёр. Единственное действие клиента — продиктовать код из SMS.
- Путать ключи и хосты.
X-Partner-Key→.../api/partner; per-orgX-API-Key→.../api/v1. Перепутать — типовая причина401. - Путать телефоны. Кассир —
7XXXXXXXXXX(авторизация Kaspi), плательщик —8XXXXXXXXXX(кому выставляете счёт). - Ждать активации тарифа по счёту автоматически. У
tariff/invoiceавтоактивации нет — тариф выдаёт владелец ApiPay вручную после банковского перевода; ловитеtariff.activatedили поллингtariff/payments. - Проверять HMAC по перепарсенному JSON. Только по сырому телу — пересериализация ломает подпись.
- Не сохранить one-time
key/webhook_secret. Показываются один раз; иначе только перевыпуск.
Частые вопросы
Заходит ли клиент в кабинет ApiPay?
Нет, ни разу. У клиента нет отдельного аккаунта ApiPay: организацию, X-API-Key и вебхук держит партнёр. Единственное действие клиента — продиктовать код из Kaspi-SMS при авторизации кассира.
Чем tariff/invoice отличается от tariff/pay?
tariff/pay — push-счёт через Kaspi на телефон плательщика, активация асинхронная после оплаты. tariff/invoice — PDF-«Счёт на оплату» для юрлица, оплата банковским переводом, тариф активируется вручную владельцем ApiPay. Это два независимых способа оплатить один тариф.
Как узнать, что тариф по счёту активирован?
Придёт вебхук tariff.activated на webhook_url партнёра. Если он не дошёл (после ретраев), тот же факт виден поллингом GET .../tariff/payments: статус платежа payment_method=invoice переходит pending → completed.
Куда приходят вебхуки об оплате счетов клиента?
На per-org webhook_url клиента (ключа, создавшего счёт). Партнёрский webhook_url в доставке invoice/refund не участвует — на него идёт только tariff.activated.
Можно ли всё протестировать без реального Kaspi?
Да. Песочница партнёра детерминированно эмулирует онбординг, авторизацию кассира, счета и вебхуки без реального Kaspi и SMS (магические номера, OTP 0000). Тот же код затем работает в production.