С чего начать
Есть два способа принимать платежи. Выберите подходящий:
Аутентификация
Все запросы к /api/v1/* требуют API-ключ в заголовке X-Api-Key. Ключи создаются в панели управления. Эндпоинт /api/health не требует авторизации.
Типы ключей
| Префикс | Режим | Описание |
|---|---|---|
sk_live_ | live | Реальные платежи через Kaspi |
sk_test_ | test | Тестовые платежи, деньги не списываются |
Режимы работы
Режим определяется автоматически по API-ключу. В test-режиме платёж создаётся в БД, но запрос в Kaspi не отправляется — статус можно менять вручную через панель.
Ошибки
Все /api/* маршруты всегда возвращают JSON независимо от заголовков запроса. Тело ответа содержит поле error.
| Код | Значение | Причина |
|---|---|---|
401 | Unauthorized | Неверный или отсутствующий API ключ |
403 | Forbidden | Организация заблокирована |
404 | Not Found | Ресурс или эндпоинт не найден |
405 | Method Not Allowed | Неверный HTTP-метод |
422 | Unprocessable Entity | Ошибка валидации — подробности в details |
429 | Too Many Requests | Превышен лимит запросов |
502 | Bad Gateway | Не удалось подключиться к внешнему URL |
500 | Server Error | Внутренняя ошибка или недоступность Kaspi |
Формат ошибки
{ "error": "Validation failed", "details": { "amount": ["The amount field is required."] } }
Rate Limiting
Авторизованные запросы — 300 запросов/мин на организацию. Публичные эндпоинты (/api/health, /api/demo/*) — 30 запросов/мин по IP. При превышении:
HTTP 429 { "error": "Too many requests" }
Health Check
Публичный эндпоинт для мониторинга. Показывает статус сервиса и Kaspi Node, uptime за 24 часа и 7 дней, среднее время ответа.
Ответ 200
{ "status": "ok", "timestamp": "2026-06-07T10:00:00+05:00", "timezone": "Asia/Almaty", "version": "1.0.0", "kaspi_node": { "online": true, "last_check": "2026-06-07T09:59:00+05:00", "response_ms": 142, "avg_ms": 138, "uptime_24h": "99.8%", "uptime_7d": "99.5%" } }
Платежи
Возвращает пагинированный список платежей организации, отсортированный по дате создания (новые первыми).
Query-параметры
| Параметр | Тип | Описание |
|---|---|---|
status | string | Фильтр по статусу: PENDING, SUCCESS, FAILED, EXPIRED, CANCELLED |
type | string | Фильтр по типу: qr или invoice |
date_from | date | С даты (YYYY-MM-DD) |
date_to | date | По дату (YYYY-MM-DD) |
per_page | integer | Записей на страницу (макс. 100, по умолч. 20) |
page | integer | Номер страницы |
Ответ 200
{ "data": [ { "id": "01J4KXYZ...", "status": "SUCCESS", "type": "qr", "amount": 5000, "currency": "KZT", "comment": "Заказ #99", "mode": "live", "paid_at": "2026-06-07T09:15:00+05:00", "created_at": "2026-06-07T09:10:00+05:00" } ], "meta": { "total": 248, "per_page": 20, "current_page": 1, "last_page": 13 } }
Создаёт QR-платёж или Invoice (выставление счёта на телефон клиента).
Тело запроса
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
type | string | да | qr или invoice |
amount | number | да | Сумма в тенге (мин. 1) |
cashier_id | integer | нет | ID кассира — см. GET /api/v1/cashiers. Если не указан — выбирается первый активный кассир организации |
customer_phone | string | да для invoice | Телефон клиента (77XXXXXXXXX). Рекомендуем предварительно проверить через /customers/lookup |
comment | string | нет | Комментарий, макс. 255 символов |
Пример — Invoice
{ "type": "invoice", "amount": 1500, "cashier_id": 3, "customer_phone": "77781234567", "comment": "Заказ #42" }
Пример — QR
{ "type": "qr", "amount": 5000, "cashier_id": 3, "comment": "Оплата услуг" }
Ответ 201
{ "id": "01J4KXYZ...", "status": "PENDING", "type": "qr", "amount": 5000, "currency": "KZT", "mode": "live", "qr_url": "https://api.qrserver.com/...", // Картинка QR для отображения (type=qr) "qr_pay_url": "https://pay.kaspi.kz/pay/...", // Прямая ссылка для оплаты (type=qr) "created_at": "2026-06-07T09:00:00+05:00" }
Возвращает актуальный статус платежа. Статус обновляется автоматически через polling.
Статусы
PENDING SUCCESS FAILED EXPIRED CANCELLED
Ответ 200
{ "id": "01J4KXYZ...", "status": "SUCCESS", "type": "invoice", "amount": 1500, "currency": "KZT", "customer_phone": "77781234567", "comment": "Заказ #42", "mode": "live", "qr_url": null, "qr_pay_url": null, "kaspi_payment_id": 15865082326, "paid_at": "2026-06-07T09:03:21+05:00", "created_at": "2026-06-07T09:00:00+05:00", "updated_at": "2026-06-07T09:03:21+05:00" }
Отменяет invoice-платёж со статусом PENDING. Только для type=invoice.
Ответ 200
{ "success": true, "status": "CANCELLED" }
/refund.Оформляет возврат по QR-платежу со статусом SUCCESS. Только для type=qr.
Тело запроса
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
amount | number | да | Сумма возврата в тенге (не более суммы платежа) |
Ответ 200
{ "success": true, "refund_amount": 1500, "payment_id": "01J4KXYZ..." }
Кассиры
Возвращает список активных кассиров организации. Используйте id из ответа как cashier_id при создании платежа.
Ответ 200
[ { "id": 3, "name": "Касса №1", "phone": "7701234567", "status": "active" } ]
Клиенты
Проверяет наличие клиента в Kaspi Pay по номеру телефона. Используйте перед созданием invoice чтобы убедиться что клиент зарегистрирован в Kaspi.
Тело запроса
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
phone | string | да | Номер телефона клиента (+77001234567 или 77001234567) |
cashier_id | integer | нет | ID кассира. По умолчанию — первый активный |
Пример запроса
{ "phone": "+77781234567" }
Ответ 200 — клиент найден
{ "found": true, "name": "Иванов Иван", "phone": "+77781234567" }
Ответ 200 — клиент не найден
{ "found": false, "name": null, "phone": "+77781234567" }
"found": false ответ всегда 200. Поле found — единственный надёжный признак наличия клиента.Checkout — виджет оплаты
Самый простой способ принять платёж. Вы создаёте сессию, получаете ссылку и перенаправляете клиента. Клиент сам выбирает способ оплаты. После оплаты возвращается к вам.
Как это выглядит
Вкладка «По номеру»
Вкладка «QR-код»
Флоу интеграции
POST /api/v1/checkout → получает checkout_url2. Перенаправляете клиента на
checkout_url3. Клиент выбирает способ оплаты и платит
4. После оплаты клиент →
success_url5. Ваш сервер получает вебхук
payment.success
Тело запроса
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
amount | number | да | Сумма в тенге |
comment | string | нет | Назначение платежа (показывается клиенту) |
cashier_id | integer | нет | ID кассира. По умолчанию — первый активный |
success_url | url | нет | Редирект после успешной оплаты |
cancel_url | url | нет | Редирект при отмене |
expires_in | integer | нет | Время жизни сессии в минутах (5–1440, по умолч. 30) |
Пример запроса
{ "amount": 3500, "comment": "Заказ #99", "success_url": "https://myshop.kz/success", "cancel_url": "https://myshop.kz/cancel", "expires_in": 15 }
Ответ 201
{ "session_token": "abc123xyz...", "checkout_url": "https://pay.example.kz/pay/abc123xyz...", "amount": 3500, "currency": "KZT", "expires_at": "2026-06-07T10:15:00+05:00", "mode": "live" }
payment.success для подтверждения оплаты на сервере — не полагайтесь только на редирект по success_url.Пример кода — PHP
$response = Http::withHeaders(['X-Api-Key' => $apiKey]) ->post('https://pay.example.kz/api/v1/checkout', [ 'amount' => 3500, 'comment' => 'Заказ #99', 'success_url' => route('order.success'), 'cancel_url' => route('order.cancel'), ]); return redirect($response->json('checkout_url'));
Возвращает текущий статус checkout-сессии. Полезно для проверки — истекла ли сессия и был ли совершён платёж.
Ответ 200
{ "session_token": "abc123xyz...", "checkout_url": "https://pay.example.kz/pay/abc123xyz...", "status": "pending", // pending | paid | expired "amount": 3500, "currency": "KZT", "comment": "Заказ #99", "mode": "live", "payment_id": null, // ID платежа после оплаты "expired": false, "expires_at": "2026-06-07T10:15:00+05:00", "created_at": "2026-06-07T10:00:00+05:00" }
Вебхуки
Система отправляет POST-запрос на ваш URL при изменении статуса платежа. Повторные попытки: 3 раза с задержкой 10с → 30с → 60с.
Ответ 200
[ { "id": 1, "url": "https://myshop.kz/webhooks/kaspi", "events": ["payment.success", "payment.failed"], "status": "active", "created_at": "2026-06-01T10:00:00+05:00" } ]
Тело запроса
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
url | string | да | HTTPS URL для получения событий |
events | array | да | Список событий (см. ниже) |
Пример запроса
{ "url": "https://myshop.kz/webhooks/kaspi", "events": ["payment.success", "payment.failed", "payment.expired"] }
Ответ 201
{ "id": 1, "url": "https://myshop.kz/webhooks/kaspi", "events": ["payment.success", "payment.failed", "payment.expired"], "secret": "s3cr3t_показывается_один_раз" }
secret — он показывается только при создании. Используйте для верификации подписи входящих запросов.Обновляет URL и/или список событий без пересоздания вебхука (секрет не меняется).
Тело запроса
| Поле | Тип | Обяз. | Описание |
|---|---|---|---|
url | string | нет | Новый URL |
events | array | нет | Новый список событий |
Ответ 200
{ "id": 1, "url": "https://myshop.kz/webhooks/v2", "events": ["payment.success"], "updated_at": "2026-06-07T11:00:00+05:00" }
Отправляет тестовый запрос на URL вебхука с фиктивными данными ("test": true). Полезно для проверки доступности вашего сервера и корректности обработки подписи.
Ответ 200 — успешно доставлено
{ "success": true, "response_code": 200, "message": "Тестовый вебхук доставлен" }
Ответ 502 — не удалось подключиться
{ "success": false, "message": "Ошибка подключения: Connection refused" }
Ответ 200
{ "message": "Webhook deleted" }
📡 События вебхуков
Доступные события для подписки:
payment.success payment.failed payment.expired payment.cancelled payment.refunded
Структура входящего запроса
// POST на ваш URL { "event": "payment.success", "payment_id": "01J4KXYZ...", "created_at": "2026-06-07T09:03:21+05:00", "data": { "id": "01J4KXYZ...", "status": "SUCCESS", "type": "qr", "amount": 5000, "currency": "KZT", "customer_phone": null, "comment": "Заказ #99", "paid_at": "2026-06-07T09:03:21+05:00", "created_at": "2026-06-07T09:00:00+05:00" } }
POST /webhooks/{id}/test) в теле будет "test": true. Используйте это поле чтобы игнорировать тестовые события в бизнес-логике.🔒 Верификация подписи
Каждый входящий вебхук содержит заголовок X-Gateway-Signature вида hmac-sha256=<hex>. Используйте secret из ответа при создании вебхука для проверки.
PHP
$secret = 'ваш_secret_из_ответа'; $body = $request->getContent(); $signature = $request->header('X-Gateway-Signature'); // hmac-sha256=abc123... $expected = 'hmac-sha256=' . hash_hmac('sha256', $body, $secret); if (!hash_equals($expected, $signature)) { abort(401, 'Invalid signature'); }
Node.js
const crypto = require('crypto'); const secret = 'ваш_secret_из_ответа'; const body = JSON.stringify(req.body); const signature = req.headers['x-gateway-signature']; const expected = 'hmac-sha256=' + crypto .createHmac('sha256', secret) .update(body) .digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) { return res.status(401).json({ error: 'Invalid signature' }); }
Python
import hmac, hashlib secret = b'ваш_secret_из_ответа' body = request.get_data() signature = request.headers.get('X-Gateway-Signature', '') expected = 'hmac-sha256=' + hmac.new(secret, body, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, signature): abort(401)