С чего начать

Есть два способа принимать платежи. Выберите подходящий:

ВАРИАНТ 1
⚙️

Прямой API

Вы сами управляете UI. Получаете QR-картинку или отправляете invoice — и показываете всё на своей странице.

✅ Полный контроль над интерфейсом
✅ Встраивается в любой сайт/приложение
⚠️ Нужно самостоятельно реализовать UI
Эндпоинты: POST /api/v1/payments · GET /api/v1/payments/{id}
ВАРИАНТ 2
🪟

Checkout — виджет оплаты

Вы создаёте сессию, отправляете клиента на нашу страницу — он сам выбирает телефон или QR и оплачивает. Потом возвращается к вам.

✅ Минимум кода — 1 запрос и редирект
✅ Виджет уже готов, красиво выглядит
✅ Таймер, polling, редирект — всё внутри
Эндпоинт: POST /api/v1/checkout
Большинству интеграций подходит Вариант 2. Вариант 1 нужен если хотите встроить оплату прямо внутри своего приложения без переходов.

Аутентификация

Все запросы к /api/v1/* требуют API-ключ в заголовке X-Api-Key. Ключи создаются в панели управления. Эндпоинт /api/health не требует авторизации.

X-Api-Key: sk_live_xxxxxxxxxxxxxxxxxxxxxxxx
Не передавайте API-ключ на фронтенд. Используйте только на сервере.

Типы ключей

ПрефиксРежимОписание
sk_live_liveРеальные платежи через Kaspi
sk_test_testТестовые платежи, деньги не списываются

Режимы работы

Режим определяется автоматически по API-ключу. В test-режиме платёж создаётся в БД, но запрос в Kaspi не отправляется — статус можно менять вручную через панель.

Ошибки

Все /api/* маршруты всегда возвращают JSON независимо от заголовков запроса. Тело ответа содержит поле error.

КодЗначениеПричина
401UnauthorizedНеверный или отсутствующий API ключ
403ForbiddenОрганизация заблокирована
404Not FoundРесурс или эндпоинт не найден
405Method Not AllowedНеверный HTTP-метод
422Unprocessable EntityОшибка валидации — подробности в details
429Too Many RequestsПревышен лимит запросов
502Bad GatewayНе удалось подключиться к внешнему URL
500Server 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

GET /api/health Без авторизации

Публичный эндпоинт для мониторинга. Показывает статус сервиса и 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%"
  }
}

Платежи

GET /api/v1/payments Список платежей

Возвращает пагинированный список платежей организации, отсортированный по дате создания (новые первыми).

Query-параметры

ПараметрТипОписание
statusstringФильтр по статусу: PENDING, SUCCESS, FAILED, EXPIRED, CANCELLED
typestringФильтр по типу: qr или invoice
date_fromdateС даты (YYYY-MM-DD)
date_todateПо дату (YYYY-MM-DD)
per_pageintegerЗаписей на страницу (макс. 100, по умолч. 20)
pageintegerНомер страницы

Ответ 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
  }
}
POST /api/v1/payments Создать платёж

Создаёт QR-платёж или Invoice (выставление счёта на телефон клиента).

Тело запроса

ПолеТипОбяз.Описание
typestringдаqr или invoice
amountnumberдаСумма в тенге (мин. 1)
cashier_idintegerнетID кассира — см. GET /api/v1/cashiers. Если не указан — выбирается первый активный кассир организации
customer_phonestringда для invoiceТелефон клиента (77XXXXXXXXX). Рекомендуем предварительно проверить через /customers/lookup
commentstringнетКомментарий, макс. 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"
}
GET /api/v1/payments/{id} Статус платежа

Возвращает актуальный статус платежа. Статус обновляется автоматически через 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"
}
POST /api/v1/payments/{id}/cancel Отменить invoice

Отменяет invoice-платёж со статусом PENDING. Только для type=invoice.

Ответ 200

{ "success": true, "status": "CANCELLED" }
QR-платежи отменить нельзя — они истекают автоматически через 5 минут. Для возврата после оплаты используйте /refund.
POST /api/v1/payments/{id}/refund Возврат средств

Оформляет возврат по QR-платежу со статусом SUCCESS. Только для type=qr.

Тело запроса

ПолеТипОбяз.Описание
amountnumberдаСумма возврата в тенге (не более суммы платежа)

Ответ 200

{
  "success":       true,
  "refund_amount": 1500,
  "payment_id":    "01J4KXYZ..."
}
Возврат доступен только для QR-платежей. Invoice-платежи не поддерживаются Kaspi API.

Кассиры

GET /api/v1/cashiers Список активных кассиров

Возвращает список активных кассиров организации. Используйте id из ответа как cashier_id при создании платежа.

Ответ 200

[
  {
    "id":      3,
    "name":    "Касса №1",
    "phone":   "7701234567",
    "status":  "active"
  }
]

Клиенты

POST /api/v1/customers/lookup Проверить номер телефона

Проверяет наличие клиента в Kaspi Pay по номеру телефона. Используйте перед созданием invoice чтобы убедиться что клиент зарегистрирован в Kaspi.

Тело запроса

ПолеТипОбяз.Описание
phonestringдаНомер телефона клиента (+77001234567 или 77001234567)
cashier_idintegerнетID кассира. По умолчанию — первый активный

Пример запроса

{
  "phone":  "+77781234567"
}

Ответ 200 — клиент найден

{
  "found":  true,
  "name":   "Иванов Иван",
  "phone":  "+77781234567"
}

Ответ 200 — клиент не найден

{
  "found":  false,
  "name":   null,
  "phone":  "+77781234567"
}
Даже при "found": false ответ всегда 200. Поле found — единственный надёжный признак наличия клиента.

Checkout — виджет оплаты

Самый простой способ принять платёж. Вы создаёте сессию, получаете ссылку и перенаправляете клиента. Клиент сам выбирает способ оплаты. После оплаты возвращается к вам.

Как это выглядит

Вкладка «По номеру»

К оплате

3 500

⏱ 14:59 осталось

По номеру
QR-код
+7 (778) 123 45 67
✓ Кахрамон Н.
Оплатить

Вкладка «QR-код»

К оплате

3 500

⏱ 14:59 осталось

По номеру
QR-код

Отсканируйте в приложении Kaspi

Флоу интеграции

1. Ваш сервер → POST /api/v1/checkout → получает checkout_url
2. Перенаправляете клиента на checkout_url
3. Клиент выбирает способ оплаты и платит
4. После оплаты клиент → success_url
5. Ваш сервер получает вебхук payment.success
POST /api/v1/checkout Создать сессию оплаты

Тело запроса

ПолеТипОбяз.Описание
amountnumberдаСумма в тенге
commentstringнетНазначение платежа (показывается клиенту)
cashier_idintegerнетID кассира. По умолчанию — первый активный
success_urlurlнетРедирект после успешной оплаты
cancel_urlurlнетРедирект при отмене
expires_inintegerнетВремя жизни сессии в минутах (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'));
GET /api/v1/checkout/{token} Статус сессии

Возвращает текущий статус 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с.

GET /api/v1/webhooks Список вебхуков

Ответ 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"
  }
]
POST /api/v1/webhooks Создать вебхук

Тело запроса

ПолеТипОбяз.Описание
urlstringдаHTTPS URL для получения событий
eventsarrayдаСписок событий (см. ниже)

Пример запроса

{
  "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 — он показывается только при создании. Используйте для верификации подписи входящих запросов.
PATCH /api/v1/webhooks/{id} Обновить вебхук

Обновляет URL и/или список событий без пересоздания вебхука (секрет не меняется).

Тело запроса

ПолеТипОбяз.Описание
urlstringнетНовый URL
eventsarrayнетНовый список событий

Ответ 200

{
  "id":          1,
  "url":         "https://myshop.kz/webhooks/v2",
  "events":      ["payment.success"],
  "updated_at":  "2026-06-07T11:00:00+05:00"
}
POST /api/v1/webhooks/{id}/test Тестовый пинг

Отправляет тестовый запрос на URL вебхука с фиктивными данными ("test": true). Полезно для проверки доступности вашего сервера и корректности обработки подписи.

Ответ 200 — успешно доставлено

{
  "success":        true,
  "response_code":  200,
  "message":        "Тестовый вебхук доставлен"
}

Ответ 502 — не удалось подключиться

{
  "success":  false,
  "message":  "Ошибка подключения: Connection refused"
}
DEL /api/v1/webhooks/{id} Удалить вебхук

Ответ 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)