Документация 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 минут
- Зарегистрируйтесь, в разделе «Сайты» добавьте сайт и скопируйте ключ qk_test_ — тестовый режим работает сразу.
- Запустите код ниже — покупатель попадёт на готовую страницу оплаты.
- Примите webhook: тестовые события приходят на тестовый webhook URL сайта.
- Для настоящих платежей: заполните анкету о бизнесе, после одобрения подключите кассу Kaspi Pay в кабинете и переключитесь на ключ qk_live_.
<?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 -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, если бессрочно).
{
"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 -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.
{
"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 часов).
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
// Подпись проверяется по СЫРОМУ телу запроса — не по 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 = доставлено
// 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();
});
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:
- Одновременные запросы выстраиваются в очередь — в Kaspi создаётся только один QR.
- Если оплата уже прошла — 409 order_already_paid.
- Если ожидает платёж того же типа и суммы — 200 и этот платёж.
- Если ожидает платёж с другой суммой — 409 payment_in_progress.
- Иначе создаётся новая попытка; прежние 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: Тестовые сценарии.
Вход кассира
Проще всего подключить кассу в кабинете. Но если хотите делать это из своей админ-панели, отправьте эти три запроса с ключом сайта — пути, тела и коды ошибок такие же, как у шлюза.
# 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 в теле) необязателен, но настоятельно рекомендуем: без него ключ создадим сами, и повтор после обрыва связи может сделать второй возврат.
# Полный возврат: 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_..."
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, с НОВЫМ ключом. |
Правило для клиента
Если в ответе есть refund — решает его status. failed — ничего не вернулось, повторите с НОВЫМ ключом. uncertain — деньги могли уйти: не повторяйте; итог AinHouse Pay сама узнает у Kaspi (до суток) — дождитесь webhook или проверьте GET /payments/{id}/refunds. Исключения: в 409 refund_in_progress refund (pending или uncertain) — другой, открытый возврат этого платежа: ваш запрос не выполнен, ключ свободен — повторите тем же ключом, когда тот возврат завершится. В 409 idempotency_key_conflict 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 — решает последнее событие.
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.
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). |
# Сценарий только для этого запроса (ключ 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.
<!-- Кнопка «Оплатить через 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).
Выполняйте заказ только по webhook
Событие paid — только подсказка для интерфейса: его может подделать кто угодно в браузере. Отдавайте товар только после подписанного webhook payment.success. data-amount тоже можно поменять в DevTools — поэтому сумму проверяет сервер, а фиксированная ссылка её вовсе игнорирует.Если 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-архив выдаём по запросу.
Установка
- В WordPress: Плагины → Добавить новый → Загрузить плагин, выберите qrkassa-woocommerce-0.1.0-beta.zip и активируйте.
- WooCommerce → Настройки → Платежи → QRkassa: вставьте API-ключ и секрет webhook из раздела «Сайты» кабинета.
- Скопируйте Webhook URL из настроек плагина в настройки сайта в кабинете: для боевого ключа — в «Webhook URL», для тестового — в «Тестовый webhook URL». Адрес выглядит так:
https://shop.kz/wp-json/qrkassa/v1/webhook - WooCommerce → Настройки → Основные: валюта — тенге (KZT). Число десятичных знаков лучше поставить 0: Kaspi не принимает тиыны.
- Если страница оформления заказа собрана блоком «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.failedpayment.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+.