Как это работает у вас
CRM: сделка перешла в «Выставить счёт»
│
POST /invoices { phone_number, amount, external_order_id_idempotency: deal_id }
│ 201, status: processing → счёт уходит покупателю push'ем в Kaspi
│
Покупатель оплачивает (у него 24 часа)
│
Вебхук invoice.status_changed { status: paid, external_order_id: ... }
│
CRM: находит сделку по external_order_id → двигает карточку в «Оплачено»
Никакого «входа в Kaspi по номеру телефона» из кода — CRM ходит только в REST ApiPay с заголовком X-API-Key; сессию с Kaspi держит ApiPay.
Минимальный контракт интеграции
Весь Kaspi API для CRM сводится к трём вызовам:
| # | Вызов | Направление | Зачем |
|---|---|---|---|
| 1 | POST /invoices |
CRM → ApiPay | Создать счёт: phone_number (формат 8XXXXXXXXXX), amount, description ≤60, external_order_id, external_order_id_idempotency |
| 2 | POST {ваш webhook} — invoice.status_changed |
ApiPay → CRM | Единственный триггер движения сделки: paid / cancelled / expired / error |
| 3 | GET /invoices/{id} |
CRM → ApiPay | Сверка/восстановление: если вебхук потерялся или нужен ручной рефреш карточки |
Этого достаточно для «карточка двигается по оплате». Возвраты (POST /invoices/{id}/refund — см. возвраты) и корзина cart_items (при Kaspi ОФД) добавляются потом, по мере надобности.
Пошаговый сетап
- Ключи: в кабинете apipay.kz создайте API-ключ и вебхук-секрет. Это разные вещи: ключ — для ваших запросов, секрет — для проверки подписи входящих вебхуков (разница).
- Песочница: прогоните весь цикл в тестовом режиме — там есть simulate-оплата. Переключение в рабочий режим обратимо и ключи не меняет: отдельного sandbox-ключа в ApiPay нет (детали).
- Создание счёта из CRM:
curl -X POST https://api.apipay.kz/api/v1/invoices \
-H "X-API-Key: ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "8701XXXXXXX",
"amount": 45000,
"description": "Сделка #4812, консультация",
"external_order_id": "deal-4812",
"external_order_id_idempotency": "deal-4812"
}'
Ответ — 201 со status: "processing": счёт создаётся асинхронно, Kaspi ещё не вызван. Не пересоздавайте счёт, пока он в processing: получатся два живых счёта, и покупатель может оплатить оба.
- Приём вебхука: эндпоинт в CRM, проверка подписи
X-Webhook-Signature: sha256=<hex>— HMAC-SHA256 по сырому телу запроса (пошагово). Отвечайте2xxбыстро, обработку — в очередь. - Движение сделки: по
external_order_idиз payload находите сделку;paid→ «Оплачено»,expired→ «Просрочен, перевыставить»,error→ на разбор менеджеру. - Обработчик должен быть идемпотентным: ApiPay повторяет доставку недоставленных вебхуков (до 11 попыток с нарастающим интервалом) — повторная доставка
paidне должна двигать сделку дважды.
Идемпотентность: главная страховка CRM
CRM ретраят HTTP-запросы, поэтому один заказ может превратиться в несколько счетов. Защита:
external_order_id_idempotencyуникален в пределах организации (до 191 символа). Повтор →409 duplicate_idempotency_keyсinvoice_idи статусом уже существующего счёта — просто используйте его.- Исключение by design: если прежний счёт уже мёртв (
expired,cancelled,error), повторный POST с тем же ключом создаст новый счёт — это штатное перевыставление, 409 не будет. - Лучший ключ — ID сделки/заказа в вашей CRM:
deal-4812. Ключ на попытку запроса (deal-4812-retry2) — антипаттерн, он отключает защиту.
Несколько CRM или отделов — несколько токенов
Если счета от одного юрлица выставляют две системы (например, CRM продаж и учётная система — для 1С есть отдельная страница), не делите один ключ — создайте отдельный API-ключ на каждую. У каждого ключа свой вебхук; в payload вебхука поле source содержит имя ключа-создателя, так что «чей счёт» всегда видно. Оплата тарифа при этом одна — тариф считается по организации, а не по числу ключей.
Грабли этого бизнеса
- Спам-цикл ретраев. No-code/ИИ-CRM без идемпотентности умеют выставить сотни счетов за минуты. Аварийный kill-switch — удалить API-ключи в кабинете, затем чинить логику ретраев и включать
external_order_id_idempotency. - Двойное движение сделки. Вебхуки доставляются повторно при ретраях — дедупите по
invoice.id+statusна своей стороне. processing— технический статус создания. Штатно он длится секунды. Дольше — либо у организации включён «Режим накопления счетов», либо выставление не доехало до Kaspi, и тогда сервис сам финализирует счёт вerrorс вебхуком. Пока счёт вprocessing, не пересоздавайте его — получатся два живых счёта; смотритеlast_kaspi_error_*вGET /invoices/{id}.- Каталог и скидки. Счёт по номеру (
POST /invoices) можно выставить одной суммойamountдаже у организации с каталогом —cart_itemsнужны только тогда, когда позиции должны попасть в чек Kaspi. Остальные правила корзины и скидок — cart_items и 422. - 401 после запуска прода. При переключении режима ключ не меняется, поэтому 401 — это почти всегда перегенерированный ключ (в кабинете или партнёрским эндпоинтом) либо ключ другой организации в конфиге CRM. Сверьте
key_hintтого ключа, что лежит в конфиге.
Рекурренты: абонементы из CRM
Если CRM ведёт абонементы (спортзал, подписка на сервис, рассрочка), не пишите свой крон — используйте подписки ApiPay. Подписка = авто-выставление счетов (не автосписание с карты — в Kaspi его нет): система сама создаёт счёт в срок, клиент подтверждает оплату push'ем, а по счёту идут обычные invoice-вебхуки. В external_subscriber_id кладите ID клиента в CRM.
Один нюанс именно для CRM: события subscription.* не пишутся в webhook-логи и не имеют ручного retry — дедупьте по (событие, subscription.id) и не теряйте. Параметры, полный жизненный цикл и цены — в статье Подписки ApiPay и на витрине Рекуррентные платежи Kaspi.
Филиалы и кассиры
Одна организация может иметь несколько касс/торговых точек. Кассу для счёта выбираете полем kaspi_connection_id в POST /invoices (по умолчанию — основная касса). Если активных касс больше одной, основная не назначена и параметр не передан — вернётся 422 connection_ambiguous: передайте явный kaspi_connection_id.
Управлять кассирами прямо из CRM можно через /connections* — но только если у ключа включён флаг can_manage_cashiers (включает владелец в кабинете; без флага — 403 cashier_management_disabled). Сама процедура подключения и переавторизации кассира — «Подключение кассира Kaspi». Разделение отчётности и счетов по точкам — Раздельная отчётность по точкам.
Мониторинг: «касса слетела» и дашборд
Фоновый мониторинг CRM строится на GET /account/health: поля connection.session_status (active / expired / error) и connection.needs_reauth — так CRM детектит «слетела Kaspi-сессия» (вебхука на это нет); при плохом статусе — алерт менеджеру и запуск переавторизации. Там же tariff (дни до конца) и invoicing.accumulating. Для дашборда — GET /invoices/stats (period today/week/month/year или start_date+end_date) → виджеты «оплачено за период», «конверсия» (conversion_rate), «в ожидании» (включает processing). GET /status — liveness без авторизации.
Важно: тайм-зоны. Счета, возвраты и подписки в ответах — UTC +00:00, но GET /tariff и GET /account/health отдают +05:00 (Asia/Almaty). Частый баг — не учесть эту разницу в дашборде.
Частые вопросы
У нас нет своего API — CRM только «умеет ходить наружу». Хватит?
Да: наружу нужен один POST (создать счёт), внутрь — один URL для вебхука. Если CRM не может принять вебхук, остаётся поллинг GET /invoices/{id} — или сборка связки без своего сервера через n8n; но вебхук надёжнее и «мгновеннее».