Документация · API v1

REST API для приёма Kaspi Pay.

Эта страница — для разработчиков, которые подключают Arzan Pay к сайту, боту, CRM или внутренней системе. Если вы владелец бизнеса — вернитесь на главную или создайте кабинет — там всё работает без программирования.

База: https://pay.arzan.cloud/api · Аутентификация: X-API-Key: <ключ> или Authorization: Bearer <ключ> · Лимит: 60 req/min на ключ

Для тестов добавляйте is_sandbox: true в тело запроса — мы не будем обращаться к реальному Kaspi.

I.

Что такое API Arzan Pay.

Один HTTP-вызов выставляет счёт в Kaspi Pay. Один webhook сообщает, что оплата прошла. Всё остальное — ваша бизнес-логика.

Arzan Pay — не банк и не эквайер. Это сервис автоматизации поверх вашего Kaspi Pay-аккаунта. Деньги клиентов идут напрямую на ваш счёт в Kaspi — мы их не храним, не получаем и не задерживаем.

  • Что вы получаете — REST API для создания счетов, QR-кодов, ссылок, возвратов и подписок.
  • Что нужно — подключённый Kaspi Pay (один раз в кабинете) и API-ключ.
  • Где деньги — на вашем счёте в Kaspi Business. Arzan Pay касается только метаданных и статусов.
  • Sandbox — пока тестируете, передавайте is_sandbox: true. Никаких реальных счетов в Kaspi.
II.

Быстрый старт за один curl.

Минимальный пример — создание счёта по номеру клиента в sandbox-режиме. Замените YOUR_API_KEY на ваш ключ из кабинета.

curl -X POST https://pay.arzan.cloud/api/v1/invoices \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 10000,
    "phone_number": "77011234567",
    "description": "Заказ № 1042",
    "external_order_id": "order_1042",
    "is_sandbox": true
  }'
const res = await fetch('https://pay.arzan.cloud/api/v1/invoices', {
  method: 'POST',
  headers: {
    'X-API-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    amount: 10000,
    phone_number: '77011234567',
    description: 'Заказ № 1042',
    external_order_id: 'order_1042',
    is_sandbox: true,
  }),
});
const invoice = await res.json();
console.log(invoice.id, invoice.status);
import requests

res = requests.post(
    'https://pay.arzan.cloud/api/v1/invoices',
    headers={'X-API-Key': 'YOUR_API_KEY'},
    json={
        'amount': 10000,
        'phone_number': '77011234567',
        'description': 'Заказ № 1042',
        'external_order_id': 'order_1042',
        'is_sandbox': True,
    },
)
invoice = res.json()
print(invoice['id'], invoice['status'])
<?php
$ch = curl_init('https://pay.arzan.cloud/api/v1/invoices');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'X-API-Key: YOUR_API_KEY',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'amount' => 10000,
        'phone_number' => '77011234567',
        'description' => 'Заказ № 1042',
        'external_order_id' => 'order_1042',
        'is_sandbox' => true,
    ]),
]);
$invoice = json_decode(curl_exec($ch), true);
echo $invoice['id'] . ' ' . $invoice['status'];

В ответе придёт invoice.id, статус (pending / processing), сумма. Клиент получит push в Kaspi приложение (или симуляцию, если sandbox).

III.

Сценарии интеграции.

Готовые рецепты по шагам для типовых задач. Copy-paste и адаптируй под свой стек.

01

Получите API-ключ

В кабинете → Разработчику → API-ключи → Создать ключ. Полный ключ покажется один раз. Сохраните его в переменные окружения вашего приложения.

02

Создайте счёт по номеру клиента

Передайте сумму в тенге, номер телефона клиента и (опционально) свой external_order_id — по нему потом сопоставите счёт с заказом в вашей CRM.

curl -X POST https://pay.arzan.cloud/api/v1/invoices \
  -H "X-API-Key: $ARZAN_KEY" \
  -H "Idempotency-Key: order_1042" \
  -d '{
    "amount": 10000,
    "phone_number": "77011234567",
    "description": "Заказ № 1042",
    "external_order_id": "order_1042"
  }'
const invoice = await fetch('https://pay.arzan.cloud/api/v1/invoices', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.ARZAN_KEY,
    'Idempotency-Key': `order_${orderId}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    amount: total,
    phone_number: customerPhone,
    description: `Заказ № ${orderId}`,
    external_order_id: `order_${orderId}`,
  }),
}).then((r) => r.json());

// Сохраните invoice.id у себя — он понадобится для возврата
await db.orders.update(orderId, { arzan_invoice_id: invoice.id });
import requests

invoice = requests.post(
    'https://pay.arzan.cloud/api/v1/invoices',
    headers={
        'X-API-Key': os.environ['ARZAN_KEY'],
        'Idempotency-Key': f'order_{order_id}',
    },
    json={
        'amount': total,
        'phone_number': customer_phone,
        'description': f'Заказ № {order_id}',
        'external_order_id': f'order_{order_id}',
    },
).json()

# Сохраните invoice['id'] у себя — он понадобится для возврата
db.orders.update(order_id, arzan_invoice_id=invoice['id'])
03

Дождитесь webhook'а об оплате

Когда клиент оплатит счёт в Kaspi, мы пришлём POST на ваш webhook URL с событием invoice.status_changed и status: "paid". Не доверяйте этому событию слепо — проверяйте подпись (см. сценарий «Проверка подписи»).

Внимание: для счёта по телефону payment_url будет null — счёт идёт push'ом в Kaspi-приложение клиента, ссылки нет. Если нужна ссылка/QR — используйте сценарий «QR / ссылка».
01

Создайте payment-link

Без номера клиента. В ответе вернутся payment_url, deeplink, qr_token — всё указывает на https://pay.kaspi.kz/pay/XXX.

curl -X POST https://pay.arzan.cloud/api/v1/payment-links \
  -H "X-API-Key: $ARZAN_KEY" \
  -H "Idempotency-Key: cart_a-2041" \
  -d '{
    "amount": 24500,
    "comment": "Корзина A-2041",
    "external_order_id": "cart_a-2041"
  }'

# Ответ:
# {
#   "id": "inv_...",
#   "payment_url": "https://pay.kaspi.kz/pay/XXXX",
#   "deeplink":    "https://pay.kaspi.kz/pay/XXXX",
#   "qr_token":    "https://pay.kaspi.kz/pay/XXXX",
#   "amount": "24500.00",
#   "status": "pending"
# }
const link = await fetch('https://pay.arzan.cloud/api/v1/payment-links', {
  method: 'POST',
  headers: {
    'X-API-Key': process.env.ARZAN_KEY,
    'Idempotency-Key': `cart_${cartId}`,
  },
  body: JSON.stringify({
    amount: cart.total,
    comment: `Корзина ${cartId}`,
    external_order_id: `cart_${cartId}`,
  }),
}).then((r) => r.json());

// link.payment_url — открывается в браузере
// link.deeplink — открывает Kaspi сразу на мобильном
return { qr_url: link.payment_url, open_in_kaspi: link.deeplink };
02

Покажите клиенту

На десктопе: сгенерируйте QR из payment_url любой QR-библиотекой ( qrcode.js, python-qrcode, phpqrcode). На мобильном: кнопка «Открыть в Kaspi» со ссылкой deeplink — тап откроет приложение Kaspi сразу.

03

Получите webhook об оплате

Аналогично сценарию по телефону — webhook прилетит с invoice.status_changed и status: "paid". По полю external_order_id вы найдёте свой заказ.

01

Найдите ID оплаченного счёта

Либо у себя в БД (вы сохранили invoice.id при создании), либо запросом GET /v1/invoices?status=paid&external_order_id=order_1042.

02

Создайте возврат

Без указания суммы — полный возврат. С суммой меньше — частичный. Можно повторять, пока сумма возвратов не превысит сумму оплаты.

# Полный возврат
curl -X POST https://pay.arzan.cloud/api/v1/invoices/inv_e80bbb3b/refund \
  -H "X-API-Key: $ARZAN_KEY"

# Частичный возврат
curl -X POST https://pay.arzan.cloud/api/v1/refunds \
  -H "X-API-Key: $ARZAN_KEY" \
  -d '{
    "invoice_id": "inv_e80bbb3b",
    "amount": 2000,
    "reason": "Возврат части товара"
  }'
// Полный возврат
await fetch(`https://pay.arzan.cloud/api/v1/invoices/${invoiceId}/refund`, {
  method: 'POST',
  headers: { 'X-API-Key': process.env.ARZAN_KEY },
});

// Частичный возврат
await fetch('https://pay.arzan.cloud/api/v1/refunds', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.ARZAN_KEY },
  body: JSON.stringify({
    invoice_id: invoiceId,
    amount: 2000,
    reason: 'Возврат части товара',
  }),
});
03

Получите webhook о возврате

Когда Kaspi подтвердит возврат, прилетит событие invoice.refunded с актуальными значениями total_refunded и available_for_refund.

01

Сохраните secret при создании webhook

Когда вы добавляете webhook URL в кабинете (или через POST /v1/webhooks), в ответе придёт secret — это HMAC-ключ. Покажем один раз. Сохраните в переменные окружения.

02

Проверяйте подпись и timestamp на каждом запросе

Подпись считается над <timestamp>.<raw_body> (Stripe-style). Timestamp приходит отдельным заголовком X-Webhook-Timestamp. Сначала проверяйте свежесть timestamp (±5 мин — защита от replay), потом подпись через timingSafeEqual.

import crypto from 'node:crypto';
import express from 'express';

const app = express();
// ВАЖНО: raw body, не используйте express.json() ДО проверки
app.use('/webhook', express.raw({ type: 'application/json' }));

app.post('/webhook', (req, res) => {
  const sig = req.headers['x-webhook-signature'];
  const ts = Number(req.headers['x-webhook-timestamp']);

  // 1. Anti-replay: timestamp не старше 5 мин
  if (!ts || Math.abs(Math.floor(Date.now() / 1000) - ts) > 5 * 60) {
    return res.status(401).send('stale or missing timestamp');
  }

  // 2. Подпись над `.`
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(`${ts}.${req.body}`)
    .digest('hex');

  if (!sig || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(401).send('bad signature');
  }

  const event = JSON.parse(req.body.toString('utf8'));
  // обработать event
  res.status(200).send('ok');
});
import hmac, hashlib, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ['WEBHOOK_SECRET']

@app.post('/webhook')
def webhook():
    raw = request.get_data()  # raw body
    sig = request.headers.get('X-Webhook-Signature', '')
    ts = request.headers.get('X-Webhook-Timestamp', '')

    # 1. Anti-replay: timestamp не старше 5 мин
    try:
        ts_int = int(ts)
    except ValueError:
        abort(401, 'bad timestamp')
    if abs(int(time.time()) - ts_int) > 5 * 60:
        abort(401, 'stale timestamp')

    # 2. Подпись над `.`
    expected = 'sha256=' + hmac.new(
        SECRET.encode(),
        f"{ts_int}.".encode() + raw,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest(sig, expected):
        abort(401, 'bad signature')

    event = request.get_json()
    return 'ok', 200
<?php
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$ts  = (int)($_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? 0);
$secret = getenv('WEBHOOK_SECRET');

// 1. Anti-replay
if (!$ts || abs(time() - $ts) > 5 * 60) {
    http_response_code(401);
    exit('stale timestamp');
}

// 2. Подпись над `.`
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $secret);

if (!hash_equals($expected, $sig)) {
    http_response_code(401);
    exit('bad signature');
}

$event = json_decode($raw, true);
http_response_code(200);
echo 'ok';
Никогда не парсите JSON до проверки подписи и не пересериализуйте тело — это сломает подпись. Большинство веб-фреймворков по умолчанию парсят body. Отключите парсинг на webhook-эндпоинте. Подпись считается над timestamp + "." + raw_body, не над одним body.
01

По одному счёту

GET /v1/invoices/{id} — получает текущее состояние из нашей БД. Если статус не обновляется долго, можно принудительно подтянуть из Kaspi через POST /v1/invoices/status/check.

curl https://pay.arzan.cloud/api/v1/invoices/inv_e80bbb3b \
  -H "X-API-Key: $ARZAN_KEY"
02

Массовая проверка

Один запрос — до 100 счетов. Удобно для крон-задачи синхронизации, если по какой-то причине пропустили webhook'и.

curl -X POST https://pay.arzan.cloud/api/v1/invoices/status/check \
  -H "X-API-Key: $ARZAN_KEY" \
  -d '{ "invoice_ids": ["inv_a", "inv_b", "inv_c"] }'
Webhook'и — основной механизм. Polling используйте только как fallback. Если webhook'и не доходят — проверьте логи в кабинете (Разработчику → Webhooks → Лог доставок).
IV.

Чек-лист перед продакшеном.

Три шага, после которых можно убрать is_sandbox и принимать реальные оплаты.

IV · 01

Тест в sandbox

Создайте тестовый счёт, проверьте список и отмену. Симулируйте оплату через /sandbox/invoices/:id/simulate.

IV · 02

Подключите webhook

Добавьте URL, сохраните secret, проверяйте подпись на raw body. Не парсите JSON до проверки.

IV · 03

Перейдите в live

Уберите is_sandbox. Передавайте external_order_id и Idempotency-Key. Не создавайте счета в healthcheck.

V.

Все эндпоинты API v1.

Полный список ниже. Дополнительно есть совместимый набор путей без префикса /v1 для миграции со старых интеграций.

МетодПутьСтатусЧто делает
POST/organizations/verifyliveПроверить API-ключ и организацию.
GET/v1/meliveТекущий мерчант, его план и возможности.
POST/v1/invoicesliveСоздать счёт по номеру — push в Kaspi-приложение клиента.
GET/v1/invoicesliveСписок счетов с пагинацией и фильтрами.
GET/v1/invoices/{id}liveПолучить счёт по ID.
POST/v1/invoices/{id}/cancelliveОтменить неоплаченный счёт.
POST/v1/invoices/status/checkliveМассово подтянуть свежие статусы из Kaspi.
POST/v1/qrliveСгенерировать QR-код (sandbox или реальный).
POST/v1/payment-linksliveСоздать ссылку оплаты без номера клиента.
POST/v1/payments/linkliveАлиас payment-links для совместимости.
POST/v1/invoices/{id}/refundliveВозврат по конкретному счёту.
GET / POST/v1/refundsliveСписок возвратов и создание возврата.
GET/v1/invoices/{id}/refundsliveВозвраты по конкретному счёту.
GET/v1/transactionsliveЛента всех операций (счета, возвраты).
GET/v1/kaspi/sessionliveСтатус подключения Kaspi: последний успех, ошибка.
POST/v1/sandbox/invoices/{id}/simulateliveСимулировать оплату/отмену/ошибку без Kaspi.
GET / POST/v1/webhooksliveУправление webhook-адресами.
DELETE/v1/webhooks/{id}liveУдалить webhook.
POST/v1/webhooks/{id}/testliveТестовая доставка на ваш URL.
GET/v1/webhook-deliveriesliveИстория доставок.
POST/v1/webhook-deliveries/{id}/retryliveПовторить доставку вручную.
GET/v1/audit-logsliveЛог действий в кабинете.
GET/v1/jobsliveОчередь задач и retry.
GET / POST/v1/subscriptionsmvpАвтоплатежи: создание, пауза, возобновление.
GET/v1/subscriptions/{id}mvpПодписка по ID.
PUT/v1/subscriptions/{id}mvpОбновить параметры подписки.
POST/v1/subscriptions/{id}/pausemvpПоставить на паузу.
POST/v1/subscriptions/{id}/resumemvpВозобновить.
POST/v1/subscriptions/{id}/cancelmvpОтменить.
GET/v1/subscriptions/{id}/invoicesmvpСчета подписки.
GET / POST/v1/catalogmvpКаталог товаров для интеграций.
VI.

Webhook события.

Каждый webhook содержит поле event с именем события и payload с актуальными данными. Подпись — в заголовке X-Webhook-Signature: sha256=<hex>, считается над <timestamp>.<raw_body>. Timestamp — в отдельном заголовке X-Webhook-Timestamp (unix-seconds), проверяйте свежесть ±5 минут. Раскройте любое событие, чтобы увидеть пример payload'а.

invoice.status_changed Статус счёта изменился: paid / cancelled / expired / processing +
{
  "event": "invoice.status_changed",
  "invoice": {
    "id": "inv_e80bbb3b",
    "external_order_id": "order_1042",
    "amount": "10000.00",
    "status": "paid",
    "description": "Заказ № 1042",
    "kaspi_invoice_id": "15660074306",
    "phone_number": "77011234567",
    "client_name": "Иван Иванов",
    "is_sandbox": false,
    "is_qr_token": false,
    "paid_at": "2026-05-24T14:12:49.689Z",
    "created_at": "2026-05-24T14:10:00.000Z"
  },
  "source": "main",
  "timestamp": "2026-05-24T14:12:50.123Z"
}
invoice.refunded Создан возврат по счёту — полный или частичный +
{
  "event": "invoice.refunded",
  "refund": {
    "id": "rfd_42",
    "amount": "2000.00",
    "status": "completed",
    "reason": "Возврат части товара",
    "created_at": "2026-05-24T15:00:00.000Z"
  },
  "invoice": {
    "id": "inv_e80bbb3b",
    "external_order_id": "order_1042",
    "amount": "10000.00",
    "total_refunded": "2000.00",
    "available_for_refund": "8000.00",
    "is_fully_refunded": false,
    "status": "paid"
  },
  "source": "main",
  "timestamp": "2026-05-24T15:00:01.000Z"
}
subscription.payment_succeeded Очередной счёт подписки оплачен клиентом +
{
  "event": "subscription.payment_succeeded",
  "subscription": {
    "id": "sub_10",
    "external_subscriber_id": "client_001",
    "phone_number": "77011234567",
    "amount": "5000.00",
    "billing_period": "monthly",
    "status": "active",
    "next_billing_at": "2026-06-24T00:00:00.000Z",
    "failed_attempts": 0,
    "is_sandbox": false
  },
  "invoice_id": "inv_subpay_001",
  "amount": "5000.00",
  "paid_at": "2026-05-24T14:00:00.000Z",
  "source": "subscription_worker",
  "timestamp": "2026-05-24T14:00:01.000Z"
}
subscription.payment_failed Очередной счёт подписки не оплачен — будет retry +
{
  "event": "subscription.payment_failed",
  "subscription": {
    "id": "sub_10",
    "phone_number": "77011234567",
    "amount": "5000.00",
    "status": "active",
    "failed_attempts": 2,
    "in_grace_period": false
  },
  "invoice_id": "inv_subpay_002",
  "reason": "Invoice expired",
  "attempt_number": 2,
  "source": "subscription_worker",
  "timestamp": "2026-05-24T14:00:01.000Z"
}
subscription.expired Подписка истекла после исчерпания grace-периода +
{
  "event": "subscription.expired",
  "subscription": {
    "id": "sub_10",
    "phone_number": "77011234567",
    "amount": "5000.00",
    "status": "expired",
    "next_billing_at": null,
    "failed_attempts": 3,
    "in_grace_period": false
  },
  "source": "subscription_worker",
  "timestamp": "2026-05-27T12:00:01.000Z"
}
webhook.test Тестовая доставка — нажали «Проверить уведомления» в кабинете +
{
  "event": "webhook.test",
  "test": true,
  "message": "Это тестовое уведомление от Arzan Pay.",
  "timestamp": "2026-05-24T14:00:00.000Z"
}

Политика повторов

  • До 11 попыток доставки: первая + 10 retry при неуспехе
  • Интервалы нарастают: 10с · 30с · 1м · 1.5м · 2м · 5м · 10м · 15м · 30м · 1ч (всего ~2 часа)
  • Ваш сервер должен ответить в течение 5 секунд (плюс 3 секунды на установление соединения)
  • HTTP 2xx = успех, любой другой код = неуспех
VII.

Подпись webhook'а.

Все примеры — выше в сценарии «Проверка подписи». Главное:

  • Заголовки: X-Webhook-Signature: sha256=<hex> и X-Webhook-Timestamp: <unix-seconds>
  • Сначала проверяйте timestamp ±5 минут (anti-replay), потом подпись
  • Подпись над timestamp + "." + raw_body, не над одним body (Stripe-style — нельзя подменить timestamp не сломав подпись)
  • Используйте HMAC-SHA256 с вашим webhook secret
  • Считайте от raw body — до парсинга JSON
  • Сравнивайте через timing-safe функцию (crypto.timingSafeEqual, hmac.compare_digest, hash_equals)
VIII.

Защита от повторов.

Если ваша система случайно отправит один и тот же запрос дважды (потерянное соединение, retry в очереди) — мы не создадим два счёта. Передавайте уникальный Idempotency-Key в заголовке.

  • Что делать — добавляйте Idempotency-Key на все create / refund операции
  • Что вернётся — при повторе с тем же ключом мы вернём тот же ответ, что и в первый раз
  • Запасной вариантexternal_order_id уникален внутри мерчанта. Дубль вернёт существующий счёт, а не создаст новый
  • Хороший ключ — UUID, либо order_${orderId} с гарантированной уникальностью
IX.

Статусы сущностей.

Счета (invoices)

StatusЗначение
pendingОжидает оплаты
processingВыставляется в Kaspi (промежуточный)
paidОплачен клиентом
cancelledОтменён вручную
cancellingВ процессе отмены (async)
expiredИстёк срок оплаты
partially_refundedЧастичный возврат
refundedПолный возврат
errorKaspi не смог обработать — смотрите error_message

Возвраты (refunds)

StatusЗначение
pendingСоздан, в очереди на обработку
processingОбрабатывается Kaspi
completedУспешно возвращён
failedОшибка возврата (детали в error_message)

Подписки (subscriptions)

StatusЗначение
activeАктивна, списания по расписанию
pausedПриостановлена, списания не выполняются
cancelledОтменена, списания прекращены навсегда
expiredИстекла после grace-периода без оплаты

Периоды списания подписок

PeriodЗначение
dailyКаждый день
weeklyКаждую неделю
biweeklyКаждые 2 недели
monthlyКаждый месяц
quarterlyКаждые 3 месяца
yearlyКаждый год
X.

Готовые сценарии.

Самые частые интеграции — что куда подключать.

X · 01

Tilda

Форма Tilda → ваш bridge-сервер → POST /v1/invoices. Статус приходит через webhook, который меняет состояние заказа.

X · 02

WordPress / Woo

Payment gateway создаёт счёт на checkout. Webhook переводит order в processing или completed.

X · 03

Telegram-бот

Бот спрашивает сумму и номер клиента, создаёт счёт. Webhook отправляет «✓ оплачено» прямо в чат.

X · 04

Bitrix24 / AmoCRM

Робот сделки → bridge → счёт. ID счёта в пользовательском поле. Webhook двигает сделку по воронке.

X · 05

CRM / POS

Используйте external_order_id, чтобы связать чек или заказ с счётом Arzan Pay.

X · 06

Внутренние системы

OpenAPI спека и Postman-коллекция — для генерации клиентов на любом языке за пять минут.

XI.

Коды ошибок.

Все ошибки возвращают JSON с полями error и человеческим message.

HTTP-статусы

HTTPКогдаЧто делать
400Bad RequestНекорректный запрос. Смотрите message и error.
401UnauthorizedAPI-ключ отсутствует, неверен или отозван.
403ForbiddenАккаунт приостановлен или нет прав на действие.
404Not FoundРесурс не существует или принадлежит другому мерчанту.
409ConflictОбъект уже в финальном состоянии или Kaspi не подключён.
422ValidationТело не прошло валидацию. Детали в details.
429Rate limitПревышен лимит 60 req/min. Используйте Retry-After.
502Kaspi errorKaspi не принял запрос. Попробуйте позже или смотрите Kaspi-статус.
503Kaspi sessionСессия Kaspi истекла. Переподключите Kaspi в кабинете.

Коды ошибок (поле error)

КодКогдаЧто делать
api_key_requiredКлюч не переданДобавьте заголовок X-API-Key или Authorization: Bearer.
invalid_api_keyКлюч неверен или отозванСоздайте новый ключ в кабинете.
invalid_phoneНеправильный формат телефонаПередавайте номер в формате 77XXXXXXXXX.
invalid_amountСумма ≤ 0 или превышает лимитСумма от 0.01 до 99 999 999.99.
validation_errorБизнес-валидацияСмотрите details для конкретных полей.
invoice_not_foundСчёт не найденПроверьте ID и принадлежность к вашему мерчанту.
invoice_not_cancelableСчёт уже в финальном статусеМожно отменить только pending или processing.
kaspi_not_connectedKaspi не подключёнПодключите в кабинете → Kaspi → SMS.
kaspi_session_invalidСессия Kaspi истеклаПереподключите через SMS.
kaspi_invoice_failedKaspi не создал счётВременный сбой Kaspi — повторите позже.
rate_limit_exceeded60 req/min превышенИспользуйте Retry-After заголовок.
refund_amount_exceedsСумма возврата больше доступнойСмотрите available_for_refund у счёта.
webhook_not_foundWebhook не существуетПроверьте ID.

Формат ошибки

{
  "error": "validation_error",
  "message": "Сумма должна быть больше 0",
  "details": {
    "amount": ["Required field"]
  }
}