Тест в sandbox
Создайте тестовый счёт, проверьте список и отмену. Симулируйте оплату через
/sandbox/invoices/:id/simulate.
Документация · API v1
Эта страница — для разработчиков, которые подключают Arzan Pay к сайту, боту, CRM или внутренней системе. Если вы владелец бизнеса — вернитесь на главную или создайте кабинет — там всё работает без программирования.
База: https://pay.arzan.cloud/api ·
Аутентификация: X-API-Key: <ключ> или
Authorization: Bearer <ключ> ·
Лимит: 60 req/min на ключ
Для тестов добавляйте is_sandbox: true в тело запроса — мы не будем обращаться к реальному Kaspi.
Один HTTP-вызов выставляет счёт в Kaspi Pay. Один webhook сообщает, что оплата прошла. Всё остальное — ваша бизнес-логика.
Arzan Pay — не банк и не эквайер. Это сервис автоматизации поверх вашего Kaspi Pay-аккаунта. Деньги клиентов идут напрямую на ваш счёт в Kaspi — мы их не храним, не получаем и не задерживаем.
is_sandbox: true. Никаких реальных счетов в Kaspi.
Минимальный пример — создание счёта по номеру клиента в 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).
Готовые рецепты по шагам для типовых задач. Copy-paste и адаптируй под свой стек.
В кабинете → Разработчику → API-ключи → Создать ключ. Полный ключ покажется один раз. Сохраните его в переменные окружения вашего приложения.
Передайте сумму в тенге, номер телефона клиента и (опционально) свой 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'])
Когда клиент оплатит счёт в Kaspi, мы пришлём POST на ваш webhook URL с событием
invoice.status_changed и status: "paid". Не доверяйте этому событию слепо —
проверяйте подпись (см. сценарий «Проверка подписи»).
payment_url будет null — счёт идёт
push'ом в Kaspi-приложение клиента, ссылки нет. Если нужна ссылка/QR — используйте сценарий «QR / ссылка».
Без номера клиента. В ответе вернутся 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 };
На десктопе: сгенерируйте QR из payment_url любой QR-библиотекой (
qrcode.js, python-qrcode, phpqrcode).
На мобильном: кнопка «Открыть в Kaspi» со ссылкой deeplink — тап откроет
приложение Kaspi сразу.
Аналогично сценарию по телефону — webhook прилетит с invoice.status_changed и
status: "paid". По полю external_order_id вы найдёте свой заказ.
Либо у себя в БД (вы сохранили invoice.id при создании), либо запросом
GET /v1/invoices?status=paid&external_order_id=order_1042.
Без указания суммы — полный возврат. С суммой меньше — частичный. Можно повторять, пока сумма возвратов не превысит сумму оплаты.
# Полный возврат
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: 'Возврат части товара',
}),
});
Когда Kaspi подтвердит возврат, прилетит событие invoice.refunded с актуальными значениями
total_refunded и available_for_refund.
Когда вы добавляете webhook URL в кабинете (или через POST /v1/webhooks), в ответе придёт
secret — это HMAC-ключ. Покажем один раз. Сохраните в переменные окружения.
Подпись считается над <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';
timestamp + "." + raw_body, не над одним body.
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"
Один запрос — до 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"] }'
Разработчику → Webhooks → Лог доставок).
Три шага, после которых можно убрать is_sandbox и принимать реальные оплаты.
Создайте тестовый счёт, проверьте список и отмену. Симулируйте оплату через
/sandbox/invoices/:id/simulate.
Добавьте URL, сохраните secret, проверяйте подпись на raw body. Не парсите JSON до проверки.
Уберите is_sandbox. Передавайте external_order_id и Idempotency-Key.
Не создавайте счета в healthcheck.
Полный список ниже. Дополнительно есть совместимый набор путей без префикса /v1 для миграции со
старых интеграций.
| Метод | Путь | Статус | Что делает |
|---|---|---|---|
| POST | /organizations/verify | live | Проверить API-ключ и организацию. |
| GET | /v1/me | live | Текущий мерчант, его план и возможности. |
| POST | /v1/invoices | live | Создать счёт по номеру — push в Kaspi-приложение клиента. |
| GET | /v1/invoices | live | Список счетов с пагинацией и фильтрами. |
| GET | /v1/invoices/{id} | live | Получить счёт по ID. |
| POST | /v1/invoices/{id}/cancel | live | Отменить неоплаченный счёт. |
| POST | /v1/invoices/status/check | live | Массово подтянуть свежие статусы из Kaspi. |
| POST | /v1/qr | live | Сгенерировать QR-код (sandbox или реальный). |
| POST | /v1/payment-links | live | Создать ссылку оплаты без номера клиента. |
| POST | /v1/payments/link | live | Алиас payment-links для совместимости. |
| POST | /v1/invoices/{id}/refund | live | Возврат по конкретному счёту. |
| GET / POST | /v1/refunds | live | Список возвратов и создание возврата. |
| GET | /v1/invoices/{id}/refunds | live | Возвраты по конкретному счёту. |
| GET | /v1/transactions | live | Лента всех операций (счета, возвраты). |
| GET | /v1/kaspi/session | live | Статус подключения Kaspi: последний успех, ошибка. |
| POST | /v1/sandbox/invoices/{id}/simulate | live | Симулировать оплату/отмену/ошибку без Kaspi. |
| GET / POST | /v1/webhooks | live | Управление webhook-адресами. |
| DELETE | /v1/webhooks/{id} | live | Удалить webhook. |
| POST | /v1/webhooks/{id}/test | live | Тестовая доставка на ваш URL. |
| GET | /v1/webhook-deliveries | live | История доставок. |
| POST | /v1/webhook-deliveries/{id}/retry | live | Повторить доставку вручную. |
| GET | /v1/audit-logs | live | Лог действий в кабинете. |
| GET | /v1/jobs | live | Очередь задач и retry. |
| GET / POST | /v1/subscriptions | mvp | Автоплатежи: создание, пауза, возобновление. |
| GET | /v1/subscriptions/{id} | mvp | Подписка по ID. |
| PUT | /v1/subscriptions/{id} | mvp | Обновить параметры подписки. |
| POST | /v1/subscriptions/{id}/pause | mvp | Поставить на паузу. |
| POST | /v1/subscriptions/{id}/resume | mvp | Возобновить. |
| POST | /v1/subscriptions/{id}/cancel | mvp | Отменить. |
| GET | /v1/subscriptions/{id}/invoices | mvp | Счета подписки. |
| GET / POST | /v1/catalog | mvp | Каталог товаров для интеграций. |
Каждый webhook содержит поле event с именем события и payload с актуальными данными. Подпись —
в заголовке X-Webhook-Signature: sha256=<hex>, считается над
<timestamp>.<raw_body>. Timestamp — в отдельном заголовке
X-Webhook-Timestamp (unix-seconds), проверяйте свежесть ±5 минут. Раскройте любое событие,
чтобы увидеть пример payload'а.
{
"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"
}
{
"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"
}
{
"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"
}
{
"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"
}
{
"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"
}
{
"event": "webhook.test",
"test": true,
"message": "Это тестовое уведомление от Arzan Pay.",
"timestamp": "2026-05-24T14:00:00.000Z"
}
Все примеры — выше в сценарии «Проверка подписи». Главное:
X-Webhook-Signature: sha256=<hex> и X-Webhook-Timestamp: <unix-seconds>timestamp + "." + raw_body, не над одним body (Stripe-style — нельзя подменить timestamp не сломав подпись)crypto.timingSafeEqual, hmac.compare_digest, hash_equals)
Если ваша система случайно отправит один и тот же запрос дважды (потерянное соединение, retry в очереди) —
мы не создадим два счёта. Передавайте уникальный Idempotency-Key в заголовке.
Idempotency-Key на все create / refund операцииexternal_order_id уникален внутри мерчанта. Дубль вернёт существующий счёт, а не создаст новыйorder_${orderId} с гарантированной уникальностью| Status | Значение |
|---|---|
pending | Ожидает оплаты |
processing | Выставляется в Kaspi (промежуточный) |
paid | Оплачен клиентом |
cancelled | Отменён вручную |
cancelling | В процессе отмены (async) |
expired | Истёк срок оплаты |
partially_refunded | Частичный возврат |
refunded | Полный возврат |
error | Kaspi не смог обработать — смотрите error_message |
| Status | Значение |
|---|---|
pending | Создан, в очереди на обработку |
processing | Обрабатывается Kaspi |
completed | Успешно возвращён |
failed | Ошибка возврата (детали в error_message) |
| Status | Значение |
|---|---|
active | Активна, списания по расписанию |
paused | Приостановлена, списания не выполняются |
cancelled | Отменена, списания прекращены навсегда |
expired | Истекла после grace-периода без оплаты |
| Period | Значение |
|---|---|
daily | Каждый день |
weekly | Каждую неделю |
biweekly | Каждые 2 недели |
monthly | Каждый месяц |
quarterly | Каждые 3 месяца |
yearly | Каждый год |
Самые частые интеграции — что куда подключать.
Форма Tilda → ваш bridge-сервер → POST /v1/invoices. Статус приходит через webhook, который меняет состояние заказа.
Payment gateway создаёт счёт на checkout. Webhook переводит order в processing или completed.
Бот спрашивает сумму и номер клиента, создаёт счёт. Webhook отправляет «✓ оплачено» прямо в чат.
Робот сделки → bridge → счёт. ID счёта в пользовательском поле. Webhook двигает сделку по воронке.
Используйте external_order_id, чтобы связать чек или заказ с счётом Arzan Pay.
OpenAPI спека и Postman-коллекция — для генерации клиентов на любом языке за пять минут.
Все ошибки возвращают JSON с полями error и человеческим message.
| HTTP | Когда | Что делать |
|---|---|---|
| 400 | Bad Request | Некорректный запрос. Смотрите message и error. |
| 401 | Unauthorized | API-ключ отсутствует, неверен или отозван. |
| 403 | Forbidden | Аккаунт приостановлен или нет прав на действие. |
| 404 | Not Found | Ресурс не существует или принадлежит другому мерчанту. |
| 409 | Conflict | Объект уже в финальном состоянии или Kaspi не подключён. |
| 422 | Validation | Тело не прошло валидацию. Детали в details. |
| 429 | Rate limit | Превышен лимит 60 req/min. Используйте Retry-After. |
| 502 | Kaspi error | Kaspi не принял запрос. Попробуйте позже или смотрите Kaspi-статус. |
| 503 | Kaspi 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_connected | Kaspi не подключён | Подключите в кабинете → Kaspi → SMS. |
kaspi_session_invalid | Сессия Kaspi истекла | Переподключите через SMS. |
kaspi_invoice_failed | Kaspi не создал счёт | Временный сбой Kaspi — повторите позже. |
rate_limit_exceeded | 60 req/min превышен | Используйте Retry-After заголовок. |
refund_amount_exceeds | Сумма возврата больше доступной | Смотрите available_for_refund у счёта. |
webhook_not_found | Webhook не существует | Проверьте ID. |
{
"error": "validation_error",
"message": "Сумма должна быть больше 0",
"details": {
"amount": ["Required field"]
}
}