ApiPay.kz — плейбук интеграции для ИИ-агентов

Один документ — весь сценарий подключения приёма платежей Kaspi Pay. Для GPT, Claude, Cursor, Copilot и других ИИ-ассистентов.

Это исполняемая инструкция, а не справочник. Выполняй шаги 0–8 по порядку. Всё необходимое (адреса, формат запросов, проверка подписи, ошибки) есть прямо здесь — открывать большие файлы (openapi.json, docs.html) НЕ нужно.

Шаг 0. Как себя вести (читает ИИ-ассистент)

Шаг 1. Объясни клиенту, что такое ApiPay

Передай клиенту своими словами 3 факта:

  1. ApiPay — это автоматизация вашего же Kaspi Pay. Мы не банк и не платёжная система. Мы даём программный доступ (API) к приёму платежей через ваш Kaspi Business.
  2. Деньги идут напрямую вам в Kaspi. Мы их не храним и не трогаем — только показываем вам и вашему сайту, что клиент оплатил.
  3. Комиссии с продаж нет. Оплачивается только подписка на сервис (от 10 000 ₸/мес). После подключения — 3 дня бесплатного доступа.

Шаг 2. Проверь предпосылки — ДО написания кода

2a. Кассир Kaspi — нужен только для рабочего режима

Это не блокер для начала интеграции. Новые организации создаются в песочнице по умолчанию — там счета создаются и без подключённого кассира: деньги никуда не идут, всё имитируется. Можно прямо сейчас писать код, создавать тестовые счета и принимать вебхуки.

Кассир Kaspi обязателен только при переходе в рабочий режим (шаг 8): без активной сессии кассира реальные счета будут падать с kaspi_session_not_configured. До этого момента подключение кассира — параллельная задача клиента, она не должна тормозить разработку.

Когда клиент будет готов идти в прод, ему нужно подключить Kaspi Business как «Кассира» — это делается один раз, двумя способами на выбор. Кратко суть (передай клиенту, чтобы запустил это параллельно):

  1. Клиент регистрируется на apipay.kz (вход по номеру телефона через WhatsApp). Это обязательно для обоих способов — кассир привязывается к аккаунту ApiPay, без регистрации подключить организацию нельзя. Регистрировать нужно на личный номер владельца/руководителя — на него идут уведомления о счетах и ошибках; это не номер кассира.
  2. Клиент берёт отдельный номер телефона (SIM), не привязанный к его личному Kaspi.
  3. В приложении Kaspi Business: Профиль → Сотрудники → добавить сотрудника с ролью «Кассир» на этот номер. После добавления Kaspi пришлёт на номер кассира ссылку на скачивание приложения Kaspi Business — устанавливать ничего не нужно, это сообщение лишь означает, что сотрудник-кассир создан и готов.
  4. Способ 1 (рекомендуемый): в личном кабинете ApiPay открывает Настройки → вкладка «Авторизация Kaspi», вводит номер кассира и код из SMS — занимает около 1 минуты.
  5. Способ 2: пишет в поддержку ApiPay в WhatsApp +7 708 516 74 89 номер кассира и название организации — специалист подключит за 15–30 минут.
  6. Подробная инструкция по обоим способам: apipay.kz/connect-cashier.
Не жди подключения кассира. Иди по шагам 3–7 в песочнице — там всё работает без кассы. Параллельно дай клиенту ссылку /connect-cashier — она пригодится на шаге 8, когда будете переходить в рабочий режим.

2b. Где будет жить интеграция — нужен сервер

Определи тип проекта клиента:

Важно: X-API-Key — это секрет. Он используется ТОЛЬКО на сервере. Никогда не помещай ключ в код, который выполняется в браузере клиента (frontend) — его увидят все посетители сайта.

2c. Анкета о бизнесе и лимит на старте — предупреди клиента заранее

У новых организаций есть мягкий лимит на старте: пока не заполнена и не одобрена короткая анкета «Расскажите о вашем бизнесе», в рабочем режиме можно создать 1 реальный счёт в сутки. В песочнице лимита нет — тестируй сколько нужно, разработку это не тормозит.

Зачем это нужно (объясни клиенту причину — тогда он не воспримет проверку как недоверие): это разовая проверка ради безопасности самого продавца. Она снижает риск, что его Kaspi-кассу зацепит антифродом вместе с мошенниками, и позволяет настроить лимиты под его обороты.

Что сделать: клиент заполняет анкету в кабинете на /business-profile (~5 минут: что продаёт, где продаёт, средний чек). Одобрение обычно за 1 рабочий день — после него лимит снимается автоматически. При попытке создать второй счёт за сутки API вернёт 429 kyc_daily_limit_reachedmeta.reset_at — когда лимит сбросится). Это не блокер разработки: продолжай в песочнице.

Шаг 3. Получи доступы в личном кабинете

Это делает человек в кабинете на apipay.kz. Нужно войти под ролью Владелец или Разработчик — у роли «Менеджер» доступа к ключам нет. Точный путь по меню:

API-ключ

  1. Настройки → вкладка «Подключение».
  2. Кнопка «Создать новый ключ».
  3. Ключ показывается один раз в окне — нажми «Копировать» и сохрани сразу.

Адрес для вебхука (куда слать уведомления об оплате)

  1. Там же, на карточке созданного ключа — кнопка «Изменить».
  2. Поле «Адрес для уведомлений» — впиши публичный HTTPS-адрес обработчика (например https://ваш-сайт.kz/webhooks/apipay).

Секрет вебхука (для проверки подписи)

  1. На карточке ключа — кнопка «Создать подпись».
  2. Секрет тоже показывается один раз — скопируй сразу.
СТОП. Попроси у клиента 3 значения: API-ключ, секрет вебхука и подтверждение, что адрес вебхука вписан в кабинете. Напомни: вебхук и секрет настраиваются на каждый ключ отдельно — отдельной общей страницы «вебхуки» нет.

Сохрани значения в переменные окружения сервера (никогда — в репозиторий):

APIPAY_API_KEY=...
APIPAY_WEBHOOK_SECRET=...
APIPAY_BASE_URL=https://api.apipay.kz/api/v1

Шаг 4. Встрой создание счёта

ПараметрЗначение
Базовый адрес APIhttps://api.apipay.kz/api/v1
Авторизациязаголовок X-API-Key: ваш_ключ (только на сервере)
Content-Typeapplication/json
Лимит запросов200 запросов в минуту на ключ (отдельный лимит 60/мин — только у POST /clients/check)
Базовый адрес API всегда https://api.apipay.kz/api/v1 — он не зависит от того, на каком сайте ты читаешь эту инструкцию. Не используй адрес документации (например localhost или apipay.kz) как адрес API.

Создать счёт — POST /invoices

// выполняется на сервере
const res = await fetch('https://api.apipay.kz/api/v1/invoices', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.APIPAY_API_KEY,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    phone_number: '87001234567',     // обязательно. Формат строго 8XXXXXXXXXX (11 цифр)
    amount: 15000,                   // обязательно. Сумма в тенге
    description: 'Заказ №123',       // необязательно, до 500 символов
    external_order_id: 'order_123'   // необязательно — ваш ID заказа для сверки
  })
})
const invoice = await res.json()
// → { id, amount, status: 'processing', phone, created_at }

Клиент получит уведомление в приложении Kaspi и оплатит там. Сохрани invoice.id рядом со своим заказом.

Проверить статус — GET /invoices/{id}

Жизненный цикл статуса: processingpendingpaid (или cancelled / expired / error). Массовая проверка нескольких счетов сразу — POST /invoices/status/check с телом {"invoice_ids":[1,2,3]}.

Шаг 4.5. Залей каталог — если в чеке нужны позиции (опционально)

Этот шаг нужен, только если клиент хочет видеть в чеке Kaspi позиции (название, цена, количество), а не одну сумму. Если продаёте «на сумму» — пропусти шаг и оставайся на обычном POST /invoices.

Что делаемЭндпоинт
Найти НТИН/GTIN по штрихкоду (маркированные товары)POST /catalog/scan — 30/мин + 2000/сутки
Залить товары пачкойPOST /catalog — до 50 позиций за запрос
Подтвердить, что доехали до KaspiGET /catalog?external_refs[]= или вебхук
Маппь товары по external_ref, не по штрихкоду. Kaspi держит один товар на штрихкод, а в учётной системе под одним штрихкодом бывает несколько позиций — сверка по штрихкоду перепутает id. external_ref (код номенклатуры из системы клиента) уникален в пределах организации и однозначен.

1. Скан штрихкода — POST /catalog/scan (только для маркированных)

// выполняется на сервере
const scan = await fetch('https://api.apipay.kz/api/v1/catalog/scan', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.APIPAY_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ input: '4870000000001' })  // штрихкод
})
const { data } = await scan.json()
// data[] — кандидаты { id, name, ntin, gtin, barcode, unit_id }. Пустой data[] = товар
// не из Нацкаталога — это НЕ ошибка, заводи без ntin/gtin.

2. Залить товары — POST /catalog (батч до 50)

const res = await fetch('https://api.apipay.kz/api/v1/catalog', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.APIPAY_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    items: [
      { name: 'Ручка гелевая синяя 0.5', selling_price: 350, unit_id: 1,
        barcode: '4870000000001', external_ref: '1c-000123' },  // external_ref = ключ маппинга
      { name: 'Тетрадь 48 листов клетка', selling_price: 420, unit_id: 1,
        barcode: '4870000000002', external_ref: '1c-000124' }
    ]
  })
})
// Рабочий режим → 202. Каждый item в ответе несёт: id, status (pending/active),
// matched_existing (true = сматчен с существующим, дубля нет), ntin_missing.
Повторная заливка безопасна (идемпотентна). Если товар уже есть, ApiPay вернёт его живой id с matched_existing: true — дубли и «мёртвые» строки не создаются. Можно гонять синк по расписанию.

3. Подтвердить результат

Ответ 202 — «принято», не «готово». Финальный статус (active/ failed) узнаётся одним из двух равноправных путей:

// Путь A (просто, для 1С/on-prem): поллинг по external_ref
const check = await fetch('https://api.apipay.kz/api/v1/catalog?' +
  'external_refs[]=1c-000123&external_refs[]=1c-000124',
  { headers: { 'X-API-Key': process.env.APIPAY_API_KEY } })
const items = (await check.json()).data
// status: 'active' — товар в Kaspi; 'failed' — смотри error_code.

// Путь B (SaaS с публичным URL): вебхук catalog.item_processed приходит на твой webhook_url
// с полями { id, external_ref, kaspi_item_id, status, error_code, ntin_missing }.
Точечная сверка — до 200 значений суммарно. Больше 200 в external_refs[]/barcodes[]/ntins[]/ids[]422 catalog_match_overflow. Разбивай сверку на батчи по ~100.

Продажа с каталогом: в POST /invoices вместо amount передавай cart_items: [{ catalog_item_id, count }] — где catalog_item_id это id товара из каталога.

СТОП. Скан штрихкода (POST /catalog/scan) ходит на живую сессию кассира Kaspi и в рабочем режиме требует подключённого кассира. Заведение каталога без маркировки (без ntin/gtin) тестируется в песочнице — там лимит 1000 позиций.

Шаг 5. Принимай вебхук об оплате

Когда счёт оплачен, ApiPay сам отправляет POST на твой адрес вебхука. Тело события:

{
  "event": "invoice.status_changed",
  "invoice": {
    "id": 42,
    "external_order_id": "order_123",
    "amount": "15000.00",
    "status": "paid",
    "paid_at": "2026-05-19T08:35:00Z"
  },
  "source": "имя вашего ключа",
  "timestamp": "2026-05-19T08:35:01Z"
}

Другие события: invoice.refunded, subscription.payment_succeeded, subscription.payment_failed, subscription.grace_period_started, subscription.expired, webhook.test.

Проверка подписи — обязательно

Каждый вебхук содержит заголовок X-Webhook-Signature: sha256=<hex> — это HMAC-SHA256 от сырого тела запроса с секретом вебхука.

const crypto = require('crypto')

// rawBody — НЕОБРАБОТАННОЕ тело запроса (Buffer/строка), НЕ результат JSON.parse.
// В Express: app.post('/webhooks/apipay', express.raw({ type: 'application/json' }), ...)
function verifyWebhook(rawBody, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex')
  const got = Buffer.from(signature || '')
  const exp = Buffer.from(expected)
  if (got.length !== exp.length) return false
  return crypto.timingSafeEqual(exp, got)
}
Две частые ошибки: (1) подпись считают по уже разобранному JSON — нужно именно сырое тело; (2) обработчик отвечает дольше 5 секунд. Отвечай 2xx сразу, тяжёлую работу делай в фоне.

Повторные доставки и идемпотентность

Если твой сервер не ответил 2xx за 5 секунд (плюс до 3 секунд на соединение), ApiPay повторит доставку. Всего до 11 попыток (первая + 10 повторов) с нарастающей задержкой: 10с, 30с, 1м, 1.5м, 2м, 5м, 10м, 15м, 30м, 1ч — около 2 часов. 5xx/429/таймаут → повтор; прочие 4xx → без повтора.

Сделай обработчик идемпотентным: одно и то же событие об оплате может прийти несколько раз. Перед выдачей товара проверяй, не обработан ли уже этот invoice.id.

Если сервера нет (шаг 2b) — вместо вебхука опрашивай статус: периодически вызывай GET /invoices/{id}, пока не будет paid. Опрос полезен и при наличии вебхука — как сверка, если все 11 попыток не прошли.

Шаг 6. Протестируй интеграцию

Тестируй в тестовом режиме (песочнице) — счета не уходят в реальный Kaspi, деньги не двигаются. Новая организация в песочнице по умолчанию.

  1. Проверка вебхука. Попроси клиента в кабинете (Настройки → «Подключение», карточка ключа) нажать кнопку «Проверить уведомления» — ApiPay пошлёт на адрес тестовое событие webhook.test. Убедись, что твой обработчик его принял и подпись сошлась.
  2. Проверка оплаты. Создай тестовый счёт через POST /invoices. В песочнице оплату счёта имитируют из кабинета — попроси клиента это сделать; придёт вебхук invoice.status_changed со статусом paid.
  3. Локальная разработка. Если сервер пока на localhost, ApiPay до него не достучится — подними туннель (ngrok). Инструкция: apipay.kz/local-testing. Туннель годится только для теста в песочнице: рабочий вебхук должен быть на реальном домене (см. шаг 8).

Полностью автономный тест через API (без человека)

Если у тебя есть sandbox-ключ (X-API-Key тестовой организации, is_sandbox: true), весь цикл можно пройти программно — не дёргая человека в кабинете. Два инструмента песочницы:

# Полный автономный цикл (sandbox X-API-Key). Требуется jq.
KEY="YOUR_SANDBOX_API_KEY"; BASE="https://api.apipay.kz/api/v1"

# 1. Создать sandbox-счёт (external_order_id_idempotency защищает от дублей при ретрае)
ID=$(curl -s -X POST "$BASE/invoices" -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  -d '{"phone_number":"87770000001","amount":5000,"description":"Autotest","external_order_id_idempotency":"autotest-001"}' | jq -r '.id')

# 1a. Дождаться статуса pending — счёт создаётся в processing, переход асинхронный
#     (обычно 1-3 секунды); simulate-status требует pending. Поллинг раз в 1-2 сек.
#     (QR-счёт из POST /invoices/qr сразу pending — для него шаг не нужен.)
until [ "$(curl -s "$BASE/invoices/$ID" -H "X-API-Key: $KEY" | jq -r '.status')" = "pending" ]; do sleep 1; done

# 2. Симулировать оплату
curl -s -X POST "$BASE/invoices/$ID/simulate-status" -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" -d '{"status":"paid"}'

# 3. Проверить доставку вебхука invoice.status_changed (status: paid)
#    Если ответ пуст — проверьте по ?invoice_id=$ID без event; тип события есть в request_body.
curl -s "$BASE/webhook-logs?invoice_id=$ID&event=invoice.status_changed" \
  -H "X-API-Key: $KEY" | jq '.data[] | {event, status, response_status}'

# 4. Частичный возврат (paid → partially_refunded, в песочнице без Kaspi)
curl -s -X POST "$BASE/invoices/$ID/refund" -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" -d '{"amount":2000}'

# 5. Проверить вебхуки возврата: invoice.refunded + invoice.status_changed (partially_refunded)
curl -s "$BASE/webhook-logs?invoice_id=$ID" -H "X-API-Key: $KEY" | jq '.data[] | {event, status}'

# Другие ветки (каждая — новый sandbox-счёт, дождаться pending как в шаге 1a):
#   {"status":"cancelled"}  → invoice.status_changed (cancelled)
#   {"status":"expired"}    → invoice.status_changed (expired)
#   {"status":"error","error_message":"Тест"} → error_code: sandbox_simulated_error
# QR-счёт: POST /invoices/qr с телом {"amount":5000,"description":"QR autotest"}
#   (phone_number не нужен; счёт сразу pending) + {"status":"qr_scanned"} →
#   invoice.qr_scanned (qr_substate: scanned, статус остаётся pending;
#   повторный qr_scanned → 400 already_scanned)

Sandbox-вебхуки доставляются с 3 попытками (backoff 5с/15с; в рабочем режиме — до 11 попыток ~2ч), успех — любой HTTP 2xx. Критерий успеха теста: по каждому шагу в /webhook-logs есть ожидаемое событие со status: success. Готовый копипаст-промпт для тест-агента — на apipay.kz/prompts.

СТОП. Действия в кабинете («Проверить уведомления», имитация оплаты) выполняет человек. Дай ему точные указания и дождись результата, затем проверь, что событие дошло до кода.

Шаг 7. Отладка

Счёт не создаётся — таблица ошибок POST /invoices

HTTP / поле errorПричинаЧто сделать
422, ошибка в phone_numberТелефон не в формате 8XXXXXXXXXX Ровно 8 и 10 цифр, без +7, пробелов и скобок
401Неверный, отсутствующий или истёкший X-API-Key Проверь ключ и заголовок (шаг 3)
400 organization_requiredОрганизация не подключена Вернись к шагу 2 — подключить кассира
400 Organization not found or not verified Организация есть, но не верифицирована (рабочий режим) Дождаться верификации организации; пока тестируй в песочнице
400 kaspi_session_not_configured Сессия Kaspi не настроена (только рабочий режим — в песочнице эта ошибка не возникает) Клиенту подключить кассира: кабинет → Настройки → «Авторизация Kaspi», либо поддержка WhatsApp. Подробно — шаг 2a и /connect-cashier
503 kaspi_session_invalid Сессия Kaspi истекла или неисправна (только рабочий режим) Переподключить кассу по SMS (см. ниже)
422 connection_ambiguousУ организации несколько касс, основная не выбрана Передать в запросе kaspi_connection_id нужной кассы
400 sandbox_invoice_limitЛимит тестовых счетов исчерпан Очистить песочницу в кабинете
429Превышен лимит запросов (200/мин на ключ; 60/мин — только у POST /clients/check) Подожди и повтори; смотри retry_after
429 kyc_daily_limit_reached Молодая орг: до одобрения анкеты о бизнесе — 1 реальный счёт/сутки (песочница без лимита) Клиенту заполнить анкету на /business-profile (~5 мин), лимит снимется после одобрения. В meta.reset_at — когда сбросится. См. шаг 2c
403 kyc_rejected Приём платежей закрыт по итогам проверки бизнеса (статус орг blocked) Не повторяемая. Клиенту написать в поддержку WhatsApp, если считает это ошибкой
422 webhook_url_requires_domain / webhook_url_tunnel_forbidden Рабочий вебхук для ещё не одобренной орг: указан IP или туннель (ngrok и подобные) Указать адрес на реальном домене (публичный HTTPS). Туннель — только для теста в песочнице (шаг 6)

Полная таблица ошибок (QR-счета, возвраты) — в docs.html.

Счёт создался (201), но статус стал error

Создание счёта асинхронное: POST /invoices возвращает 201 со статусом processing, дальше счёт уходит в Kaspi в фоне. Если что-то пошло не так на стороне Kaspi — это не ошибка HTTP, а статус error у счёта. Проверяй через GET /invoices/{id}:

У ApiPay есть канонический каталог error_code (стабильные слаги, с пометкой, какие имеет смысл повторять): например client_not_found — номер не зарегистрирован в Kaspi (попроси клиента дать номер с установленным приложением Kaspi, повтор не поможет), network_unavailable / kaspi_throttled — Kaspi временно недоступен или троттлит (повтори создание счёта позже). Полный список слагов и их retryable-разметку смотри в разделе Error Codes в docs.html.

Сессия кассира Kaspi (kaspi_session_invalid)

Сессия кассира стабильна и обычно живёт месяцами — ежедневно или по расписанию переподключать кассу не нужно. Прерваться она может только при конкретных событиях: владелец вошёл в Kaspi с другого устройства или Kaspi сам сбросил сессию. В этом случае API отдаёт 503 kaspi_session_invalid. Ретраи запроса не помогут: владелец организации один раз переподключает кассу по SMS — кабинет, Настройки → «Авторизация Kaspi», либо через поддержку. Инструкция: /connect-cashier.

Оплата прошла, но подтверждение не пришло

Открой в кабинете Настройки → «Лог уведомлений» — там видно каждую отправку вебхука: адрес, HTTP-код ответа твоего сервера, отправленное тело и полученный ответ. Это главный инструмент диагностики:

Шаг 8. Переход в рабочий режим

Когда тесты в песочнице прошли:

СТОП. Кассир Kaspi обязателен именно здесь. Если клиент ещё не подключил кассира — самое время. Без активной сессии кассира любой счёт в рабочем режиме сразу падает с kaspi_session_not_configured (см. шаг 7). Инструкция: /connect-cashier. Дождись подтверждения, что кассир подключён, и только после этого переключай режим.
  1. Кассир Kaspi подключён по /connect-cashier, подписка оплачена (после подключения — 3 дня бесплатно).
  2. Анкета о бизнесе заполнена на /business-profile (~5 мин). До её одобрения молодая орг создаёт лишь 1 реальный счёт/сутки (429 kyc_daily_limit_reached) — заполни заранее, чтобы к запуску лимит уже сняли. Зачем это — см. шаг 2c.
  3. Адрес вебхука — на реальном домене (публичный HTTPS, не IP и не туннель). Для ещё не одобренной орг ngrok/IP в проде дадут 422 webhook_url_requires_domain/webhook_url_tunnel_forbidden. Туннель из шага 6 — только для теста в песочнице.
  4. В кабинете, Настройки → «Подключение» — переключатель ТЕСТОВЫЙ → РАБОЧИЙ режим.
  5. В рабочем режиме счета уходят в реальный Kaspi, клиенты платят настоящими деньгами.
Если по итогам проверки бизнеса организация получила статус blocked, создание счёта отдаёт 403 kyc_rejected — это терминальный отказ. Анкету повторно подавать нельзя; клиенту нужно написать в поддержку WhatsApp, если он считает это ошибкой.

Готово — интеграция приёма платежей завершена.


Краткий справочник

ЧтоЗначение
Базовый адрес APIhttps://api.apipay.kz/api/v1
АвторизацияX-API-Key (заголовок, только на сервере)
Создать счётPOST /invoices
Статус счётаGET /invoices/{id}, POST /invoices/status/check
Отмена / возвратPOST /invoices/{id}/cancel, POST /invoices/{id}/refund
Подписки (рекуррент)POST /subscriptions + /pause, /resume, /cancel
Подпись вебхукаX-Webhook-Signature: sha256=…, HMAC-SHA256 от сырого тела
ПоддержкаWhatsApp +7 708 516 74 89

Полная документация (нужна редко — этого плейбука достаточно для типовой интеграции): docs.html · apipay-api-docs.md · openapi.json · llms.txt

CRM и платформам, которые подключают чужих мерчантов: см. Partner API (другой механизм — X-Partner-Key).