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

Один HTTP-запрос — и Kaspi QR готов, после оплаты приходит webhook. Контракт совпадает с kaspi-contract v1 шлюза: клиент, написанный под него, работает с AinHouse Pay без изменений.

Введение

  • Базовый адрес: https://pay.ainhouse.kz/api/v1
  • Аутентификация: Authorization: Bearer qk_live_… или qk_test_…. Ключи показываются один раз в разделе «Сайты» кабинета — сохраните их.
  • Тело и ответ — JSON. Сумма — целые тенге (не тиыны), больше 0.
  • orderId — id вашего заказа, шаблон ^[A-Za-z0-9._:-]{1,64}$.
  • Время — ISO-8601 с поясом: значения Kaspi в +05:00, наши в UTC (Z).
  • Лимит: 120 запросов в минуту на ключ.

PHP за 5 минут

  1. Зарегистрируйтесь, в разделе «Сайты» добавьте сайт и скопируйте ключ qk_test_ — тестовый режим работает сразу.
  2. Запустите код ниже — покупатель попадёт на готовую страницу оплаты.
  3. Примите webhook: тестовые события приходят на тестовый webhook URL сайта.
  4. Для настоящих платежей: заполните анкету о бизнесе, после одобрения подключите кассу Kaspi Pay в кабинете и переключитесь на ключ qk_live_.
PHP
<?php
// Без библиотек: PHP 7.4+ и расширение curl.
$apiKey = getenv('QRKASSA_KEY'); // qk_test_… для проверки, qk_live_… в работе

$ch = curl_init('https://pay.ainhouse.kz/api/v1/payments/qr');
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 20,
    CURLOPT_HTTPHEADER => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'amount' => 12500,              // целые тенге
        'orderId' => 'order-1042',      // ваш номер заказа
        'description' => 'Заказ №1042',
    ]),
]);

$payment = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

if ($status === 200 || $status === 201) {
    // Проще всего — отправить покупателя на готовую страницу оплаты.
    header('Location: ' . $payment['checkoutUrl']);
    exit;
}

// 402 — подписка не оплачена, 503 — касса не подключена: покажите
// «Kaspi временно недоступен» и предложите другой способ оплаты.
error_log('QRkassa: ' . $status . ' ' . ($payment['error'] ?? ''));
curl
curl -X POST https://pay.ainhouse.kz/api/v1/payments/qr \
  -H "Authorization: Bearer qk_test_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{"amount": 12500, "orderId": "order-1042", "description": "Заказ №1042"}'

Эндпоинты

Все под https://pay.ainhouse.kz/api/v1.

Запрос Что делает
POST /payments/qr Создать QR. Тело: amount, orderId, description?. 201 — новый, 200 — уже ожидающий QR этого orderId.
POST /payments/invoice Счёт в приложение Kaspi покупателя. Тело: amount, orderId, phone (10 цифр), comment?.
GET /payments/{paymentId} Платёж. ?refresh=1 — заново запрашивает статус у Kaspi.
GET /payments?orderId=… Все попытки по orderId, новые первыми: {"payments": [...]}.
POST /payments/{paymentId}/cancel Отмена только счёта. QR не отменяется — истекает сам за 5 минут (409 not_cancellable).
POST /payments/{paymentId}/refund Полный или частичный возврат оплаченного QR. Тело: amount?, reason?; заголовок Idempotency-Key. Подробно — в разделе «Возвраты».
GET /payments/{paymentId}/refunds Возвраты платежа, новые первыми: {"refunds": [...]}.
POST /links Ссылка на оплату. Тело: amount, description, orderPrefix?. Ответ: {"url", "slug"}.
GET /status Состояние кассы, сессии и подписки. Запроса в Kaspi не делает.

GET /status

Совпадает с форматом /status шлюза (merchant, session, pending), плюс subscription. state: not_started | trial | active | free | grace | expired | blocked.

not_started — пробный период ещё не начался: он начнётся при первом подключении кассы Kaspi, поэтому trialEndsAt — null. В этом состоянии работают тестовые ключи и запросы на чтение, а для live-платежей нужна касса (503 cashier_not_connected).

free — AinHouse Pay открыл вам бесплатный доступ: API работает как обычно, freeUntil — конец срока (null, если бессрочно).

JSON
{
  "merchant": {
    "id": "01JC8Z5W4X3Y2Z1A0B9C8D7E6F",
    "name": "ИП «Арман»",
    "orgName": "ИП АРМАНОВ А.",
    "organizationIdn": "900101300123",
    "phoneHint": "+7 777 *** ** 41"
  },
  "session": { "state": "active", "authorizedAt": "2026-11-08T09:12:00Z", "lostAt": null },
  "pending": 3,
  "gatewayTime": "2026-11-08T11:40:00Z",
  "appVersion": "26.1029",
  "subscription": { "state": "trial", "trialEndsAt": "2026-11-15T09:00:00Z", "paidUntil": null, "freeUntil": null }
}

POST /links

curl
curl -X POST https://pay.ainhouse.kz/api/v1/links \
  -H "Authorization: Bearer qk_live_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" \
  -d '{"amount": 5000, "description": "Консультация", "orderPrefix": "consult"}'

# 201 → {"url": "https://pay.ainhouse.kz/l/a8K2mQ7x", "slug": "a8K2mQ7x"}

Объект Payment

Этот объект возвращают все эндпоинты платежей и webhook. К контракту добавлены только checkoutUrl и mode.

Payment
{
  "paymentId": "18517303624",
  "type": "qr",
  "mode": "live",
  "orderId": "order-1042",
  "projectId": "01JC8Z6V8Q3K2M1N0P9R7S6T5U",
  "merchantId": "01JC8Z5W4X3Y2Z1A0B9C8D7E6F",
  "amount": 12500,
  "status": "QrTokenCreated",
  "statusDesc": "",
  "event": null,
  "qrUrl": "https://qr.kaspi.kz/5348323210109151130255947803787637336075",
  "payUrl": "https://pay.kaspi.kz/pay/5348323210109151130255947803787637336075",
  "checkoutUrl": "https://pay.ainhouse.kz/pay/01JC8Z7A1B2C3D4E5F6G7H8J9K",
  "receiptUrl": "https://receipt.kaspi.kz/preview/cashier?extTranId=QR18517303624&present=modal",
  "expiresAt": "2026-11-08T16:44:12.000+05:00",
  "createdAt": "2026-11-08T11:39:12.207Z",
  "updatedAt": "2026-11-08T11:39:12.207Z",
  "finishedAt": null,
  "refundedAmount": 0
}
paymentId Id платежа (Kaspi QrOperationId, строка). У тестового платежа — наш id.
mode live или test — каким ключом создан.
qrUrl Кодируйте в QR-картинку ИМЕННО его: читают и Kaspi, и приложения других банков.
payUrl Для кнопки «Оплатить в Kaspi» на телефоне — открывает приложение Kaspi.
checkoutUrl Наша готовая страница оплаты: QR, сумма, отсчёт, возврат на ваш сайт после оплаты.
status Сырой статус Kaspi: QrTokenCreated → Wait → Processed (успех) или CancelledByUser.
event null пока ожидает, в конце: payment.success | failed | expired | lost.
expiresAt Срок QR: 5 минут с создания, с часовым поясом +05:00.
amount Запрошенная вами сумма, целые тенге.
refundedAmount Сумма успешных возвратов (по умолчанию 0). Возврат не меняет event: полностью возвращённый платёж остаётся payment.success.

Webhook

На каждое финальное событие — один POST на webhook URL сайта. Тело событий payment.* — объект Payment с заполненным event. Ответ 2xx — доставлено; иначе (или нет ответа за 10 с) повторяем: 5 с, 30 с, 2 мин, 10 мин, 30 мин, 1 ч, 2 ч, 2 ч (≈ 6 часов).

HTTP
POST /your/webhook HTTP/1.1
Content-Type: application/json
X-Webhook-Event: payment.success
X-Webhook-Id: 18517303624:payment.success
X-Webhook-Signature: sha256=<hex HMAC-SHA256(secret, сырое тело запроса)>
X-Webhook-Mode: live
User-Agent: QRkassa/1

X-Webhook-Mode: live или test. Боевые события приходят на webhook URL сайта с подписью секретом webhook; тестовые — только на тестовый webhook URL, с отдельным тестовым секретом. На боевом сервере храните только боевой секрет: тогда «оплата» с тестовым ключом не пройдёт проверку подписи. Редиректы не выполняем, на адреса внутренних сетей не отправляем.

payment.success Деньги пришли. Сверьте amount с заказом — при расхождении нужен человек.
payment.failed Покупатель отменил или банк отказал. Заказ открыт, можно создать новый QR.
payment.expired За время жизни QR никто не оплатил. Как failed.
payment.lost Итог неизвестен (долгий разрыв сессии и т. п.). Деньги могли пройти — не закрывайте заказ, отметьте для человека.

Другие события: конверт

У событий возвратов, автоплатежей и проверочного события тело — конверт: event на верхнем уровне (всегда равен X-Webhook-Event), объект самого события и, если есть, payment. Поэтому обработчик сначала смотрит body.event: не читайте конверт как Payment. Заголовки, подпись и повторы — как выше. На незнакомый event можно ответить 2xx и пропустить его.

payment.refunded · refund.failed Возврат прошёл или не прошёл. Тело: event, payment, refund, timestamp. Возвраты
subscription.* События автоплатежей. Тело: event, subscription, charge, payment, timestamp. Автоплатежи
webhook.test Проверочное событие из кабинета. Тело: event, site, mode, timestamp. Тестовые сценарии

Проверка подписи

PHP
<?php
// Подпись проверяется по СЫРОМУ телу запроса — не по json_encode(json_decode(...)).
$secret = getenv('QRKASSA_WEBHOOK_SECRET');
$raw = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);

if (! hash_equals($expected, $signature)) {
    http_response_code(403);
    exit;
}

$payment = json_decode($raw, true);
$deliveryId = $_SERVER['HTTP_X_WEBHOOK_ID'] ?? '';

// Один и тот же X-Webhook-Id может прийти повторно — обработайте его один раз.
if ($payment['event'] === 'payment.success') {
    // Сверьте $payment['amount'] с суммой заказа $payment['orderId'], затем отметьте заказ оплаченным.
}

http_response_code(200); // любой 2xx = доставлено
Laravel
// routes/web.php. Уберите путь из CSRF в bootstrap/app.php:
// $middleware->validateCsrfTokens(except: ['qrkassa/webhook']);
Route::post('/qrkassa/webhook', function (Illuminate\Http\Request $request) {
    $raw = $request->getContent(); // сырое тело
    $expected = 'sha256=' . hash_hmac('sha256', $raw, config('services.qrkassa.webhook_secret'));

    abort_unless(hash_equals($expected, (string) $request->header('X-Webhook-Signature')), 403);

    $payment = json_decode($raw, true);

    if ($payment['event'] === 'payment.success') {
        $order = App\Models\Order::where('number', $payment['orderId'])->firstOrFail();

        // Тот же X-Webhook-Id может прийти повторно: открываем доступ один раз.
        if (! $order->paid && $order->amount === $payment['amount']) {
            $order->update(['paid' => true]);
            $order->user->courses()->attach($order->course_id); // доступ открыт
        }
    }

    return response()->noContent();
});
Node.js / Express
import crypto from 'node:crypto';
import express from 'express';

const app = express();

// express.raw — чтобы получить тело как Buffer, до любого JSON-парсинга.
app.post('/qrkassa/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const expected = 'sha256=' + crypto
        .createHmac('sha256', process.env.QRKASSA_WEBHOOK_SECRET)
        .update(req.body)
        .digest('hex');
    const got = req.get('X-Webhook-Signature') ?? '';

    const valid = got.length === expected.length
        && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));

    if (!valid) return res.sendStatus(403);

    const payment = JSON.parse(req.body.toString('utf8'));
    // идемпотентно по req.get('X-Webhook-Id')

    res.sendStatus(200);
});

Идемпотентность

Для одного сайта и одного orderId:

  1. Одновременные запросы выстраиваются в очередь — в Kaspi создаётся только один QR.
  2. Если оплата уже прошла — 409 order_already_paid.
  3. Если ожидает платёж того же типа и суммы — 200 и этот платёж.
  4. Если ожидает платёж с другой суммой — 409 payment_in_progress.
  5. Иначе создаётся новая попытка; прежние failed/expired/lost остаются в истории.

Тестовый режим

Платёж с ключом qk_test_ никогда не уходит в Kaspi. Он создаётся в статусе QrTokenCreated и через 10 секунд сам становится Processed — webhook payment.success приходит на тестовый webhook URL (X-Webhook-Mode: test, с тестовым секретом); если тестовый URL пуст, событие не отправляется. qrUrl и payUrl ведут на нашу тестовую страницу: там можно отметить «Оплачен» или «Не оплачен».

Тестовые платежи видны в кабинете с пометкой «test» и не зависят от подписки.

10 секунд и «оплачен» — значения по умолчанию. Другой итог, задержку, ошибку создания, сбой webhook или итог тестового возврата задайте на странице «Тестовый режим» в кабинете или заголовком X-Sandbox-Scenario: Тестовые сценарии.

Вход кассира

Проще всего подключить кассу в кабинете. Но если хотите делать это из своей админ-панели, отправьте эти три запроса с ключом сайта — пути, тела и коды ошибок такие же, как у шлюза.

curl
# 1. Начать вход — Kaspi готовит отправку SMS
curl -X POST https://pay.ainhouse.kz/api/v1/merchant/login/start -H "Authorization: Bearer qk_live_ВАШ_КЛЮЧ"
# 201 → {"loginId": "6f1c…", "phoneHint": "+7 777 *** ** 41"}

# 2. Номер кассира (10 цифр) — Kaspi отправит SMS
curl -X POST https://pay.ainhouse.kz/api/v1/merchant/login/phone -H "Authorization: Bearer qk_live_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" -d '{"loginId": "6f1c…", "phone": "7771234541"}'
# 200 → {"smsSent": true}

# 3. Код из SMS
curl -X POST https://pay.ainhouse.kz/api/v1/merchant/login/otp -H "Authorization: Bearer qk_live_ВАШ_КЛЮЧ" \
  -H "Content-Type: application/json" -d '{"loginId": "6f1c…", "otp": "1234"}'
# 200 → {"ok": true, "merchant": {...}, "authorizedAt": "…Z"}
  • 404 login_not_found — loginId не найден или старше 10 минут.
  • 422 phone_mismatch — номер не совпадает с закреплённым номером кассира.
  • 200 {"smsSent": false, "errorCode": "OldVersionToUpdate"} — Kaspi требует новую версию: мы обновляем, повторите через 1–2 часа.
  • 200 {"ok": false, "errorMessage": "…"} — неверный код из SMS.
  • 409 organization_mismatch — номер вошёл в кассу другой организации; ничего не сохранено.
  • 429 too_many_attempts — больше 5 попыток за 10 минут.
  • 403 live_key_required — только с ключом qk_live_: тестовый ключ не трогает кассу.
  • POST /merchant/logout — отключить кассу.

Возвраты

Оплаченный QR можно вернуть полностью или частично: деньги вернутся на счёт покупателя через Kaspi. По одному платежу — несколько частичных возвратов, но одновременно только один; в сумме не больше платежа. Вернуть можно и в кабинете, со страницы платежа.

POST /payments/{paymentId}/refund

Тело: amount? (пусто — всё оставшееся), reason? (покупатель не видит, в Kaspi не уходит). Заголовок Idempotency-Key (или idempotencyKey в теле) необязателен, но настоятельно рекомендуем: без него ключ создадим сами, и повтор после обрыва связи может сделать второй возврат.

curl
# Полный возврат: amount не передаём. Частичный — amount в тенге.
# Idempotency-Key: свой ключ на каждую попытку (повтор с тем же ключом вернёт тот же возврат).
curl -X POST https://pay.ainhouse.kz/api/v1/payments/18517303624/refund \
  -H "Authorization: Bearer qk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: rf-order-1042-1" \
  -d '{"amount": 5000, "reason": "Товар вернули"}'

# История возвратов платежа (новые первыми)
curl https://pay.ainhouse.kz/api/v1/payments/18517303624/refunds \
  -H "Authorization: Bearer qk_live_..."
JSON
HTTP/1.1 201 Created

{
  "refund": {
    "refundId": "01jcz8v6qd3m2k1n0p9r7s6t5u",
    "paymentId": "18517303624",
    "orderId": "order-1042",
    "mode": "live",
    "amount": 5000,
    "status": "succeeded",
    "reason": "Товар вернули",
    "error": null,
    "message": null,
    "source": "api",
    "createdAt": "2026-11-10T09:12:03.120Z",
    "finishedAt": "2026-11-10T09:12:05.871Z"
  },
  "payment": {
    "paymentId": "18517303624",
    "orderId": "order-1042",
    "amount": 12500,
    "event": "payment.success",
    "refundedAmount": 5000
  }
}
HTTP Ответ Что значит
201 refund.status = succeeded Kaspi вернул деньги. В теле refund и payment (refundedAmount обновлён).
202 refund.status = pending | uncertain Итог пока неизвестен. Не повторяйте — дождитесь webhook payment.refunded или refund.failed.
200 idempotentReplay: true Тот же запрос с этим Idempotency-Key уже был: текущее состояние возврата.
400 validation amount — целое положительное (тенге), reason — до 255 символов, ключ — ^[A-Za-z0-9._:-]{1,64}$.
404 not_found Платежа нет или он другого сайта / режима.
409 not_refundable Вернуть нельзя, причина в cause: not_paid — не оплачен, fully_refunded — уже возвращён полностью, not_qr — оплачен по счёту, kaspi_not_refundable — Kaspi не разрешает (возможно, истёк срок), unknown_at_gateway — платёж слишком старый для возврата через API (верните в Kaspi Pay), billing — оплата подписки AinHouse Pay (возвращается вручную).
409 amount_exceeds_available Сумма больше доступной к возврату; в ответе available.
409 refund_in_progress У платежа есть незавершённый возврат (одновременно только один). refund в теле — тот, другой возврат; ваш запрос не выполнен, ключ свободен.
409 idempotency_key_conflict Этот ключ уже использован для платежа с другой суммой или причиной; refund в теле — тот, прежний возврат.
403 merchant_blocked Аккаунт заблокирован. Окончание подписки возвраты не останавливает.
403 refunds_disabled Возвраты временно отключены на платформе. Ничего не возвращено, ключ свободен.
503 cashier_session_lost | cashier_not_connected Касса не подключена — войдите заново в кабинете и повторите с НОВЫМ ключом.
502 kaspi_error Kaspi отказал (например, не хватает средств на счёте Kaspi); message — текст Kaspi, refund.status = failed. Если Kaspi не отказал, а не смог ответить, вместо message будет cause: kaspi_unavailable (Kaspi временно недоступен, повторите позже) или operation_not_found (Kaspi не нашёл операцию, верните в Kaspi Pay).
502 gateway_error Сервис связи с Kaspi не выполнил запрос (например, связь сайта с Kaspi ещё не готова). Ничего не возвращено; повторите позже — если в теле есть refund, с НОВЫМ ключом.

GET /payments/{paymentId}/refunds

Возвраты платежа, новые первыми: {"refunds": [...]}.

Объект Payment и заказ

  • В объекте Payment есть refundedAmount — сумма успешных возвратов. Полностью возвращён: refundedAmount = amount.
  • event не меняется: полностью возвращённый платёж остаётся payment.success. Новый QR на тот же orderId — 409 order_already_paid; для новой продажи нужен новый orderId.
  • В v1 возвращаются только QR-платежи; оплату по счёту возвращайте в приложении Kaspi Pay.
  • Комиссия Kaspi не возвращается, а на вашем счёте Kaspi должно хватать средств на возврат. Срок возврата у Kaspi ограничен.

Webhook: payment.refunded, refund.failed

Тело — конверт: {event, payment, refund, timestamp}; event равен заголовку X-Webhook-Event, X-Webhook-Id = {paymentId}:{event}:{refundId}. Подпись и повторы — как у других событий. Доставки независимы: payment.refunded может прийти раньше повторного payment.success — сверяйте payment.refundedAmount со своими данными. Если итог возврата выяснился позже (например, возврат отметили как «не прошёл», а Kaspi его подтвердил), после refund.failed по тому же refundId может прийти payment.refunded — решает последнее событие.

HTTP
X-Webhook-Event: payment.refunded
X-Webhook-Id: 18517303624:payment.refunded:01jcz8v6qd3m2k1n0p9r7s6t5u
X-Webhook-Signature: sha256=…
X-Webhook-Mode: live

{
  "event": "payment.refunded",
  "payment": { "paymentId": "18517303624", "orderId": "order-1042", "amount": 12500, "event": "payment.success", "refundedAmount": 5000 },
  "refund": { "refundId": "01jcz8v6qd3m2k1n0p9r7s6t5u", "amount": 5000, "status": "succeeded", "source": "api" },
  "timestamp": "2026-11-10T09:12:05.871Z"
}

С тестовым ключом: тестовый платёж возвращается без Kaspi, результат — по настройке «Тестовый режим» в кабинете или заголовку X-Sandbox-Scenario (refund_failed, refund_uncertain, refund_pending).

Автоплатежи

Счета Kaspi постоянным клиентам по расписанию: абонементы, аренда, оплата обучения. Деньги не списываются автоматически — в каждом периоде на номер клиента приходит счёт (invoice), клиент подтверждает его в приложении Kaspi. Клиенты добавляются в кабинете (раздел «Автоплатежи») или загружаются из Excel/CSV.

Расписание

  • Период: ежедневно, еженедельно, ежемесячно или ежегодно. Время счёта — по времени Казахстана (по умолчанию 10:00). Ежемесячное расписание считается от даты начала: в месяце без 31-го счёт уходит в последний день месяца.
  • Неоплаченный счёт: через 24 часа новый счёт, максимум 3 попытки, затем subscription.needs_attention.
  • Старый долг не копится: период старше 48 часов проходит без счёта, за время паузы счета не выставляются.
  • Счета на один номер уходят не чаще раза в 10 минут — чтобы Kaspi не ограничил частые счета.

Webhook: subscription.*

Ведите логику автоплатежей по событиям subscription.*. Каждый счёт также даёт обычное событие payment.* (orderId = rec:…) — для автоплатежей его можно игнорировать. Заголовки и подпись — как у платежей; тело — конверт: event, subscription, charge, payment, timestamp. Номера клиента в теле нет. События тестового клиента приходят только на тестовый webhook URL.

subscription.payment_succeeded
POST /qrkassa/webhook
X-Webhook-Event: subscription.payment_succeeded
X-Webhook-Id: rec:01jd5k3m8x7q2w9e4r6t1y0u5i:subscription.payment_succeeded:01jd5p0a2b3c4d5e6f7g8h9j0k
X-Webhook-Signature: sha256=…
X-Webhook-Mode: live

{
  "event": "subscription.payment_succeeded",
  "subscription": {
    "subscriptionId": "01jd5k3m8x7q2w9e4r6t1y0u5i",
    "externalId": "cust-77",
    "projectId": "01jc8v2n4m6b8x0z2c4v6b8n0m",
    "merchantId": "01jc8t1r3e5w7q9a1s3d5f7g9h",
    "mode": "live",
    "name": "Айгүл С.",
    "amount": 15000,
    "description": "Абонемент {month}",
    "period": "monthly",
    "startDate": "2026-11-15",
    "billTime": "10:00",
    "nextChargeAt": "2026-12-15T05:00:00.000Z",
    "cyclesLimit": 12,
    "periodsIssued": 1,
    "periodsPaid": 1,
    "status": "active",
    "attentionReason": null,
    "finishReason": null
  },
  "charge": {
    "chargeId": "01jd5p0a2b3c4d5e6f7g8h9j0k",
    "periodIndex": 1,
    "periodStart": "2026-11-15",
    "attempt": 1,
    "status": "paid",
    "error": null,
    "message": null,
    "paymentId": "18517303624",
    "amount": 15000
  },
  "payment": { "paymentId": "18517303624", "type": "invoice", "orderId": "rec:01jd5k3m8x7q2w9e4r6t1y0u5i:20261115:1", "event": "payment.success", "…": "…" },
  "timestamp": "2026-11-15T05:03:11.000Z"
}
subscription.created Клиент добавлен (в кабинете или импортом).
subscription.invoice_issued Счёт периода отправлен клиенту. charge.attempt — номер попытки.
subscription.payment_succeeded Период оплачен — продлите услугу.
subscription.payment_failed Счёт не оплачен или истёк. Если попытки остались, через 24 часа уйдёт новый счёт.
subscription.needs_attention Нужен человек: причина в subscription.attentionReason.
subscription.paused / resumed Приостановлен или возобновлён в кабинете.
subscription.period_skipped Период прошёл без счёта: charge.error = skipped_by_merchant | missed | issue_failed.
subscription.finished Завершён: finishReason = cycles_done (счета закончились) или cancelled.

X-Webhook-Id: rec:{subscriptionId}:{event}:{chargeId|ulid} — одно событие может прийти несколько раз, обработка должна быть идемпотентной. Статусы (subscription.status): active, waiting_payment, needs_attention, paused, finished.

Причины внимания

payment_failed Счёт не оплачен ни в одной попытке.
payment_lost Итог счёта неизвестен — проверьте в Kaspi Pay. Автоматического повтора нет.
cashier_session_lost / cashier_not_connected Касса не подключена — счёт повторяется каждые 15 минут.
subscription_required Закончилась подписка AinHouse Pay.
kaspi_error Kaspi не принял счёт (например, номера нет в Kaspi) — текст в charge.message.
gateway_error / site_disabled Ошибка связи или сайт отключён.

Импорт из Excel/CSV

Первая строка — названия столбцов (на казахском, русском или английском): name, phone, amount, description, period (daily | weekly | monthly | yearly или «ежемесячно», «ай сайын»), start_date (ДД.ММ.ГГГГ или ГГГГ-ММ-ДД), bill_time, cycles_limit, external_id, note. Разделитель в CSV — «;» или «,». Не больше 1000 строк. Сначала показываются ошибки по строкам, затем добавляются только верные строки.

Тестовый режим

Счёт тестового клиента не уходит в Kaspi и завершается сам, как тестовый платёж. В тестовом режиме — до 10 клиентов. Кнопка «Выставить счёт сейчас» на странице клиента позволяет проверить всю цепочку сразу, не дожидаясь периода.

Тестовые сценарии

На странице «Тестовый режим» в кабинете задайте сценарий для каждого сайта: результат тестового платежа и через сколько секунд, ошибку при создании, неудачу первых 1–8 попыток webhook, результат тестового возврата. Это влияет только на ключ qk_test_: деньги не списываются, запросы в Kaspi не уходят.

X-Sandbox-Scenario

Для одного запроса вместо настроек — заголовком: только с ключом qk_test_, с боевым игнорируется. Если сработал, в ответе будет заголовок X-Sandbox-Scenario-Applied.

paid | failed | expired | manual Результат тестового платежа (manual — только кнопками на странице оплаты).
cashier_session_lost | cashier_not_connected Создание платежа вернёт 503.
gateway_error | kaspi_error Создание платежа вернёт 502 (kaspi_error — с message).
subscription_required Создание платежа вернёт 402 (paidUntil, payUrl).
refund_failed | refund_uncertain | refund_pending POST /payments/{id}/refund: 502 kaspi_error, 202 uncertain или 202 pending (затем payment.refunded).
curl
# Сценарий только для этого запроса (ключ qk_test_):
# paid | failed | expired | manual
# cashier_session_lost | cashier_not_connected | gateway_error | kaspi_error | subscription_required
# refund_failed | refund_uncertain | refund_pending (для POST /payments/{id}/refund)
curl -i -X POST https://pay.ainhouse.kz/api/v1/payments/qr \
  -H "Authorization: Bearer qk_test_..." \
  -H "Content-Type: application/json" \
  -H "X-Sandbox-Scenario: failed" \
  -d '{"amount": 5000, "orderId": "test-1001"}'

webhook.test

Со страницы «Тестовый режим» можно отправить проверочное событие на тестовый или боевой адрес: тело {event: "webhook.test", site, mode, timestamp}, подпись — секретом этого адреса, X-Webhook-Id = test:{id}. Намеренно неудачная доставка видна в кабинете как «Тест: сбой смоделирован».

Кнопка на сайт

Поставьте на сайт кнопку «Оплатить через Kaspi» без программиста: создайте в кабинете ссылку или лист с QR и скопируйте код «Кнопка на сайт». Окно оплаты откроется прямо на сайте; деньги поступают прямо на ваш счёт Kaspi.

HTML
<!-- Кнопка «Оплатить через Kaspi»: скрипт один раз на страницу -->
<script src="https://pay.ainhouse.kz/widget.js" defer></script>

<div data-qrkassa-link="Ab12Cd34"
     data-lang="ru"
     data-theme="green"
     data-mode="modal"></div>

<!-- Ссылка с открытой суммой: сумма подставится, сервер всё равно проверит min/max -->
<div data-qrkassa-link="Xy98Zw76" data-amount="5000" data-size="lg" data-full-width></div>

<script>
  // Подсказка для интерфейса. Заказ выполняйте только по подписанному webhook.
  QRkassa.on('paid', function (event) {
    console.log('Оплачено', event.orderId, event.paymentId);
  });
</script>
data-qrkassa-link 8-символьный код ссылки или листа с QR (из адреса /l/… в кабинете). Обязателен.
data-amount Только для ссылок с открытой суммой: подставится заранее, пределы (min/max) сервер всё равно проверит. Фиксированная ссылка его игнорирует.
data-lang kk или ru. По умолчанию — <html lang>, иначе kk.
data-theme green (по умолчанию), dark или light.
data-mode modal (по умолчанию) — окно поверх сайта; tab — новая вкладка. На телефоне (уже 480 px) всегда новая вкладка.
data-label Свой текст кнопки. По умолчанию: «Kaspi арқылы төлеу» / «Оплатить через Kaspi».
data-size sm | md | lg
data-full-width Кнопка на всю ширину контейнера.

JavaScript

  • QRkassa.open({ link, amount, lang, mode }) — открыть из своей кнопки.
  • QRkassa.refresh() — если страница перерисована с новыми кнопками (SPA).
  • QRkassa.mount(element) — поставить кнопку в один элемент.
  • QRkassa.on('paid' | 'failed' | 'expired' | 'close', callback) — событие: { orderId, paymentId }. Сама кнопка тоже генерирует qrkassa:paid и т. п. (bubbles).

Если CSP вашего сайта запрещает iframe, укажите data-mode="tab". Приложение Kaspi откроется в новом окне (iOS Safari не открывает его из iframe). Виджет не использует cookie. AinHouse Pay не является официальным партнёром Kaspi; логотипа Kaspi на кнопке нет.

Коды ошибок

Тело ошибки всегда {"error": "…"}, иногда с дополнительными полями.

HTTP error Что делать
400 validation Неверные amount или orderId; подробности по полям в details.
401 unauthorized Ключа нет или он неверен (заменённый ключ перестаёт работать сразу).
402 subscription_required Подписка закончилась или аккаунт заблокирован. В ответе paidUntil и payUrl (страница оплаты в кабинете).
403 kyc_required Для live-платежей и подключения кассы нужна одобренная анкета о бизнесе (кабинет → главная). Тестовый ключ продолжает работать.
403 kyc_rejected Анкета о бизнесе отклонена: live-платежи закрыты. Напишите в поддержку.
403 merchant_blocked Аккаунт заблокирован: возврат не выполняется. Окончание подписки возвраты не останавливает.
404 not_found Платежа нет или он принадлежит другому сайту.
409 order_already_paid Этот orderId уже оплачен; в ответе payment.
409 payment_in_progress По этому orderId ожидает платёж с другой суммой.
409 not_cancellable QR нельзя отменить.
409 not_refundable Вернуть нельзя: не оплачен, уже возвращён полностью или оплачен по счёту (в поле cause).
409 amount_exceeds_available Сумма возврата больше остатка; в ответе available.
409 refund_in_progress У платежа есть незавершённый возврат — дождитесь итога.
409 idempotency_key_conflict Этот Idempotency-Key уже использован с другой суммой или причиной.
429 too_many_requests Больше 120 запросов в минуту на ключ.
502 kaspi_error Kaspi отклонил запрос; причина от Kaspi — в message.
502 gateway_error Сервис связи с Kaspi временно не ответил — повторите через минуту.
503 cashier_not_connected Касса ещё не подключена — подключите в кабинете.
503 cashier_session_lost Сессия кассы прервалась — войдите заново в кабинете. Покупателю покажите «Kaspi временно недоступен».

WordPress / WooCommerce

Бета

Плагин QRkassa for WooCommerce добавляет в магазин способ оплаты «Kaspi QR». Покупатель платит на странице оплаты QRkassa и возвращается на ваш сайт, а заказ отмечается оплаченным по подписанному webhook. Деньги приходят напрямую на ваш счёт Kaspi Pay.

Как получить плагин

На время беты zip-архив выдаём по запросу.

Установка

  1. В WordPress: Плагины → Добавить новый → Загрузить плагин, выберите qrkassa-woocommerce-0.1.0-beta.zip и активируйте.
  2. WooCommerce → Настройки → Платежи → QRkassa: вставьте API-ключ и секрет webhook из раздела «Сайты» кабинета.
  3. Скопируйте Webhook URL из настроек плагина в настройки сайта в кабинете: для боевого ключа — в «Webhook URL», для тестового — в «Тестовый webhook URL». Адрес выглядит так: https://shop.kz/wp-json/qrkassa/v1/webhook
  4. WooCommerce → Настройки → Основные: валюта — тенге (KZT). Число десятичных знаков лучше поставить 0: Kaspi не принимает тиыны.
  5. Если страница оформления заказа собрана блоком «Checkout», замените его классическим шорткодом: [woocommerce_checkout]

Настройки

API-ключ Ключ сайта qk_live_ или qk_test_. Режим определяется по началу ключа.
Секрет webhook Для qk_live_ — секрет webhook, для qk_test_ — секрет тестового webhook.
Webhook URL Поле только для чтения. Скопируйте его в настройки сайта (ниже).
Статус после оплаты «Обработка» (по умолчанию) или «Выполнен».
Журнал отладки WooCommerce → Статус → Журналы, источник «qrkassa». Ключ и секрет в журнал не попадают.
Адрес API Адрес API QRkassa: https://pay.ainhouse.kz/api/v1. Значение по умолчанию меняйте только для тестового сервера.

Как меняется статус заказа

payment.success Заказ оплачен: «Обработка» или «Выполнен». Если сумма не совпала или заказ уже был оплачен — статус «На удержании» и заметка.
payment.failed
payment.expired
Только заметка. Заказ остаётся «В ожидании оплаты», покупатель может оплатить снова.
payment.lost Статус «На удержании»: деньги могли пройти, перед отправкой проверьте в Kaspi Pay.
payment.refunded Возврат, сделанный в кабинете QRkassa, сам записывается в WooCommerce.

orderId плагина — wc-{номер заказа}. Повторная оплата того же заказа безопасна: QRkassa не спишет деньги за один заказ дважды. Если webhook задерживается, страница «Спасибо за заказ» один раз запрашивает статус у QRkassa.

Тестовый режим

  • Укажите ключ qk_test_ и секрет тестового webhook, а Webhook URL скопируйте в поле «Тестовый webhook URL» сайта.
  • Тестовый платёж не уходит в Kaspi. Через 10 секунд он оплачивается сам или отмечается вручную на тестовой странице. В заметках заказа стоит пометка [TEST].
  • WooCommerce завершает тестовый заказ по-настоящему (склад, письма) — проверяйте на тестовой копии магазина.
  • Мы не отправляем webhook на адреса внутренних сетей (localhost, LAN). В локальном магазине заказ подтверждается только проверкой на странице «Спасибо за заказ».

Возвраты из WooCommerce

  • На странице заказа нажмите «Возврат», введите сумму и нажмите кнопку возврата через «QRkassa — Kaspi QR». Полностью или частично, только целыми тенге; одновременно — один возврат.
  • Если Kaspi не подтвердил возврат сразу, плагин покажет предупреждение: не повторяйте. Когда возврат пройдёт, WooCommerce запишет его сам по webhook.
  • Возврат, сделанный в кабинете QRkassa, тоже появится в заказе WooCommerce.

Ограничения беты

  • Только классическое оформление заказа: с блоком «Checkout» плагин помечен как несовместимый.
  • Валюта магазина — только KZT; заказу с тиынами в сумме Kaspi QR не предлагается.
  • Только QR: выставления счёта в приложение Kaspi в плагине нет.
  • HPOS (новые таблицы заказов) поддерживается. PHP 7.4+, WooCommerce 7.0+.