Как связать свою CRM с Kaspi-оплатами?

Обновлено 6 июля 2026 · Решение проблем · Версия в Markdown
Содержание
  1. Как это работает у вас
  2. Минимальный контракт интеграции
  3. Пошаговый сетап
  4. Идемпотентность: главная страховка CRM
  5. Несколько CRM или отделов — несколько токенов
  6. Грабли этого бизнеса
  7. Типовой сценарий: карточка двигается сама
  8. Рекурренты: абонементы из CRM
  9. Филиалы и кассиры
  10. Мониторинг: «касса слетела» и дашборд
  11. Частые вопросы

Как это работает у вас

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 ≤500, 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 ОФД) добавляются потом, по мере надобности.

Пошаговый сетап

  1. Ключи: в кабинете apipay.kz создайте API-ключ и вебхук-секрет. Это разные вещи: ключ — для ваших запросов, секрет — для проверки подписи входящих вебхуков (разница).
  2. Песочница: прогоните весь цикл на sandbox-организации — там есть simulate-оплата. Внимание: при переходе в рабочий режим тестовые организации и их ключи удаляются — ключи прода будут новые.
  3. Создание счёта из 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, — получите два живых счёта.

  1. Приём вебхука: эндпоинт в CRM, проверка подписи X-Webhook-Signature: sha256=<hex> — HMAC-SHA256 по сырому телу запроса (пошагово). Отвечайте 2xx быстро, обработку — в очередь.
  2. Движение сделки: по external_order_id из payload находите сделку; paid → «Оплачено», expired → «Просрочен, перевыставить», error → на разбор менеджеру.
  3. Обработчик должен быть идемпотентным: 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 содержит имя ключа-создателя, так что «чей счёт» всегда видно. Оплата тарифа при этом одна — тариф считается по организации, а не по числу ключей.

Грабли этого бизнеса

  1. Спам-цикл ретраев. No-code/ИИ-CRM без идемпотентности умеют выставить сотни счетов за минуты. Аварийный kill-switch — удалить API-ключи в кабинете, затем чинить логику ретраев и включать external_order_id_idempotency.
  2. Двойное движение сделки. Вебхуки доставляются повторно при ретраях — дедупите по invoice.id + status на своей стороне.
  3. processing — не зависание. При высокой нагрузке счёт легитимно может висеть в processing дольше 60 минут; система сама ретраит. Не пересоздавайте — смотрите last_kaspi_error_* в GET /invoices/{id}.
  4. Каталог и скидки. Если у вас Kaspi ОФД, без cart_items будет 422; при своих скидках надёжнее пересчитать цену на своей стороне и передать price позиции явно — см. cart_items и 422.
  5. Ключи после прода. После перехода из песочницы ключи другие; «вчера работало, сегодня 401» — почти всегда старый ключ в конфиге CRM.

Типовой сценарий: карточка двигается сама

Обобщённый пример. У компании самописная CRM без собственного платёжного API, а вход в Kaspi возможен только по номеру телефона. Задача сводится к простому: выставлять счёт покупателю-физлицу и автоматически двигать карточку сделки, когда оплата прошла. Интеграция строится так: кнопка «Выставить счёт Kaspi» в карточке вызывает POST /invoices, где ключом идемпотентности служит deal_id сделки; вебхук-эндпоинт проверяет подпись и ставит фоновую задачу передвинуть сделку в нужный статус. Когда со временем счета начинает выставлять и вторая система (например, учётная), ей заводят отдельный токен — счета обеих систем различают по полю source в вебхуке. Ручная сверка по утрам отпадает: GET /invoices/{id} дёргается только если карточка «застряла».

Рекурренты: абонементы из CRM

Если CRM ведёт абонементы (спортзал, подписка на сервис, рассрочка), не пишите свой крон — используйте подписки ApiPay. Подписка = авто-выставление счетов (не автосписание с карты — в Kaspi его нет): система сама создаёт счёт в срок, клиент подтверждает оплату push'ем.

POST /subscriptions: phone_number, billing_period (daily/weekly/biweekly/monthly/quarterly/yearly), billing_day (1–28), сумма — amount (min 100) либо cart_items (для каталог-организации), external_subscriber_id = ID клиента в CRM, параметры ретраев (max_retry_attempts 1–10, retry_interval_hours 1–168, grace_period_days 1–30), bill_immediately (выставить первый счёт сразу).

curl -X POST https://api.apipay.kz/api/v1/subscriptions \
  -H "X-API-Key: ВАШ_КЛЮЧ" -H "Content-Type: application/json" \
  -d '{ "phone_number": "8XXXXXXXXXX", "amount": 15000,
        "billing_period": "monthly", "billing_day": 5,
        "subscriber_name": "Клиент CRM #88",
        "external_subscriber_id": "crm-client-88",
        "description": "Абонемент, ежемесячно" }'

Что система делает сама (CRM ничего не пересоздаёт): в срок выставляет счёт → по нему идут обычные invoice-вебхуки; при неоплате шлёт subscription.payment_failed и перевыставляет счёт, пока не исчерпаны попытки; затем subscription.grace_period_started; после grace — subscription.expired, и биллинг останавливается навсегда (реактивации нет — заводите новую подписку). Управление: pause/resume/cancel (resume пересчитывает next_billing_at от текущего момента, пропущенные периоды не доначисляются).

Важно: два нюанса для CRM. События subscription.* не пишутся в webhook-логи и не имеют ручного retry — дедупьте по (событие, subscription.id) и не теряйте. И известный gap: счёт подписки со status=error (например client_not_found) провалом платежа не считаетсяpayment_failed не приходит, период «пропущен»; CRM узнаёт об этом только из invoice-вебхука error. Полный жизненный цикл, кейсы и цены — на витрине Рекуррентные платежи Kaspi и в статье Подписки ApiPay.

Филиалы и кассиры

Одна организация может иметь несколько касс/торговых точек. Кассу для счёта выбираете полем kaspi_connection_id в POST /invoices (по умолчанию — основная касса). Если активных касс больше одной, основная не назначена и параметр не передан — вернётся 422 connection_ambiguous: передайте явный kaspi_connection_id.

Управлять кассирами прямо из CRM можно через /connections* — но только если у ключа включён флаг can_manage_cashiers (включает владелец в кабинете; без флага — 403 cashier_management_disabled). Переавторизация «касса слетела» — три шага: auth/initauth/send-phone (Kaspi шлёт SMS кассиру на 7XXXXXXXXXX) → auth/verify-otp. Разделение отчётности и счетов по точкам — Раздельная отчётность по точкам.

Мониторинг: «касса слетела» и дашборд

Фоновый мониторинг CRM строится на GET /account/health: поле connection.session_status — так 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; но вебхук надёжнее и «мгновеннее».

Что писать в external_order_id и чем он отличается от ..._idempotency?

external_order_id — просто ваша метка, вернётся в вебхуке для поиска сделки. external_order_id_idempotency — защитный ключ от дублей (повтор → 409). Обычно оба равны ID сделки.

Можно ли с двух CRM-аккаунтов выставлять счета от одного Kaspi?

Да — заведите каждому отдельный API-токен, так удобнее различать источники; тариф от числа токенов не меняется.

Как протестировать без реальных оплат?

В песочнице: тот же API, simulate-оплата, webhook.test для проверки подписи. Подписки прогоняются симуляциями simulate-invoice/start-simulation/stop-simulation. Перед продом перечитайте песочница и рабочий режим — ключи в проде будут новые.

Можно ли делать регулярные списания/абонементы из CRM?

Не автосписание с карты (в Kaspi его нет), а авто-выставление счетов: подписка сама создаёт счёт в срок, клиент подтверждает push'ем. Заводится через POST /subscriptions, привязка к клиенту CRM — external_subscriber_id. Истёкшую подписку не реактивировать — создавать новую. Подробно — Подписки ApiPay.

Как выставлять счета с разных касс или филиалов одной организации?

Передавайте kaspi_connection_id при создании счёта. Если активных касс больше одной и основная не назначена — без параметра вернётся 422 connection_ambiguous. Разделение отчётности по точкам — в статье Раздельная отчётность по точкам.

Интеграция с ИИ

Не настраивайте вручную — поручите ИИ

Скопируйте готовый промпт и вставьте своему ИИ-ассистенту — он подключит приём платежей примерно за 15 минут. Настраивает ИИ, а не вы.

Для разработчика. Полная спецификация API — apipay.kz/docs. Эта статья в Markdown — /guides/apipay-dlya-svoey-crm.md. Справочник фактов для ИИ-агента — apipay.kz/llms.txt.