Главная › Документация API
Reference · v1Документация API
Полный технический референс Kaspi Pay invoice API: авторизация по SMS,
создание счёта, статусы, отмена, возвраты, вебхуки с
X-Webhook-Signature: sha256=..., коды ошибок.
Правовые положения
Отказ от ответственности
Сервис PayProverkaBot (pay.proverkacheka.kz) не связан и не аффилирован с АО «Kaspi Bank» или любыми другими организациями группы Kaspi. Мы не являемся платёжным сервисом, эквайрингом или финансовой организацией. Мы не являемся партнёрами, представителями или агентами Kaspi.
PayProverkaBot предоставляет техническую услугу — программный интерфейс к функции «удалённый счёт» приложения Kaspi Pay для бизнеса от имени самого мерчанта, после его добровольной авторизации по SMS на его собственный номер.
Ответственность пользователя
Используя сервис, вы подтверждаете, что:
- Вы являетесь владельцем (или уполномоченным представителем) аккаунта Kaspi Pay для бизнеса, по которому проходит авторизация.
- Вы соблюдаете законодательство Республики Казахстан и условия Kaspi.
- Вы используете сервис в законных целях — автоматизация выставления счетов, интеграция с CRM, чат-ботами, кассовыми системами.
- Вы не используете сервис для незаконной деятельности, отмывания денег, уклонения от налогов или иных противоправных действий.
Администрация PayProverkaBot не несёт ответственности за прямые или косвенные убытки, возникшие в результате использования сервиса, блокировки аккаунта Kaspi или технических сбоев на стороне Kaspi.
Начало работы
Как получить API-ключ
- Откройте @PayProverkaBot в Telegram.
- Нажмите
/start— получите персональныйAPI_KEY. - Авторизуйтесь в Kaspi Pay:
/login→ отправьте номер телефона ИП/ТОО → введите SMS-код. - Готово — можете выставлять счета через бота (
/invoice 87019009393 1500 За кофе) или через REST API.
/apikey в боте покажет ваш ключ повторно.
Команда /account покажет, к какому аккаунту Kaspi привязана сессия.
Базовый URL и заголовки
- Базовый URL:
https://pay.proverkacheka.kz/api - Авторизация: заголовок
X-API-Key: <ваш_ключ> - Content-Type:
application/jsonдля всех POST/PUT
Что делает сервис
- Создаёт удалённый счёт Kaspi Pay — клиенту приходит push-уведомление в Kaspi.kz, где он подтверждает оплату одним нажатием.
- Возвращает
paymentIdдля отслеживания статуса. - Поллит статус в фоне и доставляет вебхук, как только клиент оплатил, отказался или счёт истёк.
- Поддерживает отмену неоплаченного счёта и возврат (полный / частичный) оплаченного.
Проверка API-ключа
Сам онбординг (SMS-логин в Kaspi) проходит через Telegram-бота — это внутренний процесс сервиса. Через API доступна только проверка выданного ключа:
GET/v1/auth/me
Возвращает информацию о привязанной сессии Kaspi.
curl https://pay.proverkacheka.kz/api/v1/auth/me \
-H "X-API-Key: $API_KEY"
Создание счёта
POST/v1/invoice/create
Создаёт удалённый счёт Kaspi Pay с корзиной товаров. Клиенту приходит push-уведомление
в Kaspi.kz. Сумма счёта рассчитывается автоматически как Σ(items[i].price × items[i].count).
Тело запроса
| Поле | Тип | Описание |
|---|---|---|
phoneNumber required | string | Номер клиента. Сервис автоматически нормализует — принимаются форматы 87019009393, 77019009393, 7019009393, +7 701 900 93 93. На вход в Kaspi уходит каноничный 8XXXXXXXXXX. На невалидный формат — 400 invalid_phone. |
items required | array | 1–50 позиций корзины. См. ниже. |
comment optional | string | Описание счёта. По умолчанию — названия позиций через запятую. |
source optional | string (≤50) | Пространство имён интеграции, например kommo, tilda или merchant_crm. |
externalUserId optional | string (≤128) | ID клиента в системе интегратора. Не заменяет телефон и не влияет на доставку счёта. |
externalUserType optional | string (≤50) | Тип ID: telegram, kommo_contact, crm_customer и т.п. Используйте вместе с source. |
externalDealId optional | string (≤100) | ID сделки/заказа. Для kommo.leadId заполняется автоматически. |
customerTgUserId optional | integer | Сокращённый вариант: нормализуется в externalUserId + externalUserType="telegram". |
Все поля внешней идентификации необязательны. Старые клиенты с phoneNumber, items и comment продолжают работать без изменений.
Структура items[i]
| Поле | Тип | Описание |
|---|---|---|
name required | string (1–200) | Название товара/услуги — показывается в чеке клиента. |
price required | number > 0 | Цена за единицу в тенге. |
count optional | integer > 0 | Количество. По умолчанию 1. |
Пример
curl -X POST https://pay.proverkacheka.kz/api/v1/invoice/create \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phoneNumber": "87019009393",
"comment": "Заказ #42",
"source": "merchant_crm",
"externalUserId": "customer-7788",
"externalUserType": "crm_customer",
"externalDealId": "deal-42",
"items": [
{ "name": "Капучино", "price": 1200, "count": 2 },
{ "name": "Чизкейк", "price": 1800, "count": 1 }
]
}'
{
"paymentId": "14918505280",
"cartId": "c8a91b...",
"amount": 4200.0,
"status": "RemotePaymentCreated",
"receiptUrl": null
}
Статус и список
GET/v1/invoice/{paymentId}
Возвращает актуальный статус и (после оплаты) ссылку на чек.
curl https://pay.proverkacheka.kz/api/v1/invoice/14918505280 \
-H "X-API-Key: $API_KEY"
{
"paymentId": "14918505280",
"status": "Processed",
"amount": "4 200 ₸",
"clientMobile": "87019009393",
"receiptUrl": "https://receipt.kaspi.kz/..."
}
Возможные статусы
| Status | Значение |
|---|---|
RemotePaymentCreated | ⏳ Ожидает оплаты клиентом |
Processed | ✅ Оплачен |
RemotePaymentRejected | ❌ Клиент отказался |
RemotePaymentCanceled | 🚫 Отменён мерчантом |
RemotePaymentExpired | ⌛ Истёк срок (~24 часа) |
Refunded | ↩️ Полностью возвращён |
PartiallyRefunded | ↩️ Частично возвращён |
GET/v1/invoice
Список активных (не финализированных) счетов, отслеживаемых сервисом.
curl https://pay.proverkacheka.kz/api/v1/invoice \
-H "X-API-Key: $API_KEY"
Отмена счёта
POST/v1/invoice/{paymentId}/cancel
Отменяет ожидающий счёт (статус RemotePaymentCreated).
Уже оплаченные — см. возврат (/refund).
curl -X POST https://pay.proverkacheka.kz/api/v1/invoice/14918505280/cancel \
-H "X-API-Key: $API_KEY"
Успех (HTTP 200):
{ "cancelled": true, "paymentId": "14918505280", "status": "RemotePaymentCanceled" }
После успешной отмены сервис сразу финализирует счёт и шлёт webhook
payment.failed с extras.reason = "merchant_cancelled".
Неуспех (обычно HTTP 502): Kaspi отклонил отмену.
Типичный текст: Operation not found (код -99000001).
Так бывает, когда клиент уже оплачивает или уже оплатил
счёт. Это не значит «деньги не прошли».
| Правило | Почему |
|---|---|
Помечайте счёт «failed / cancelled» только при HTTP 200 от /cancel | Иначе «убиваете» живой счёт локально |
Если /cancel ≠ 200 — оставьте локальный статус pending | Через секунды может прийти payment.success по тому же paymentId |
payment.success = деньги взяты → активируйте доступ | State machine с запретом failed → paid роняет оплативших клиентов |
paymentId — ключ идемпотентности | Повтор webhook с тем же id не должен второй раз начислять товар/дни |
Типичный race: счёт A выставлен → клиент оплатил в Kaspi →
снова зашёл в бота → бот /cancel A (reject) → ошибочно
пометил A failed и создал B → webhook success для A проигнорирован →
деньги есть, доступа нет.
Правильный retire-pending: if await cancel(id): mark_failed(id)
— иначе leave pending. После failed cancel полезен
GET /v1/invoice/{id}: если Processed — активируйте
(или реплеите webhook). Полный паттерн —
fallback-гайд.
Возврат
POST/v1/invoice/{paymentId}/refund
Возвращает деньги по оплаченному счёту (Processed).
Поддерживается полный и частичный возврат. Если amount опущен —
возвращается весь остаток AvailableReturnAmount.
Тело запроса
| Поле | Тип | Описание |
|---|---|---|
amount optional | number > 0 | Сумма возврата в тенге. Опустите для полного возврата. |
# Частичный возврат
curl -X POST https://pay.proverkacheka.kz/api/v1/invoice/14918505280/refund \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"amount": 1200}'
{
"paymentId": "14918505280",
"refundedAmount": 1200.0,
"isFull": false,
"remainingRefundable": 3000.0,
"refundsTotal": 1
}
GET/v1/invoice/{paymentId}/refunds
История возвратов по счёту.
Вебхуки
Подпишитесь на события платежей — сервис будет POST'ить на ваш URL JSON-пейлоад каждый раз, когда меняется финальный статус. Это рекомендуемый способ интеграции вместо поллинга.
События
| Event | Когда срабатывает |
|---|---|
payment.success | Клиент оплатил счёт |
receipt.ready | Фискальный чек и точный способ оплаты готовы |
payment.failed | Клиент отказался или мерчант отменил |
payment.expired | Истёк срок ожидания оплаты |
payment.refunded | Произведён возврат (полный или частичный) |
* | Подписка на все события сразу |
receipt.ready автоматически отправляется всем подписчикам payment.success, включая уже существующие подписки. Обработчик должен отвечать 2xx на незнакомые типы событий и игнорировать их. payment.success остаётся без изменений и всегда является основным подтверждением оплаты. Владелец магазина может выключить только это дополнительное событие в Telegram: Webhooks → Выключить чек в webhook; захват чеков и отчёты при этом продолжат работать. Администратор может изменить тот же переключатель в карточке магазина через /admin.
POST/v1/webhooks
curl -X POST https://pay.proverkacheka.kz/api/v1/webhooks \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/kaspi/webhook",
"events": ["payment.success", "payment.refunded"],
"secret": "my-shared-secret-min-8-chars",
"label": "billing-service"
}'
{ "id": 7, "url": "https://example.com/...", "events": ["payment.success","payment.refunded"], "label": "billing-service", "has_secret": true, "is_active": true }
Поле label — необязательное (до 100 символов). Сервис его не использует, а просто возвращает как есть в GET /v1/webhooks, чтобы вы могли различать подписки.
Несколько подписок на один API-ключ
Лимита нет: к одному API-ключу можно подписать сколько угодно вебхуков. Это удобно, если несколько ваших сервисов слушают события одного аккаунта Kaspi. Чтобы понимать, какой вебхук какому сервису принадлежит, используйте label (или просто разные URL) — оба поля видны в GET /v1/webhooks.
GET/v1/webhooks · DELETE/v1/webhooks/{id}
Просмотр и удаление подписок. В ответе GET у каждой подписки есть id, url, events, label, has_secret, is_active — удаляйте по id.
Формат пейлоада
POST https://example.com/kaspi/webhook
Content-Type: application/json
X-Webhook-Event: payment.success
X-Webhook-Attempt: 1
Idempotency-Key: payment.success:19:14918505280:v1
X-Webhook-Signature: sha256=<hex-hmac>
User-Agent: kaspipayinvoice-webhooks/1.0
{
"event": "payment.success",
"eventId": "payment.success:19:14918505280:v1",
"paymentId": "14918505280",
"type": "invoice",
"status": "Processed",
"amount": 4200.0,
"clientMobile": "87019009393",
"extras": {},
"timestamp": 1747049280
}
После фискализации Kaspi отправляется отдельное обогащение. HTTP-порядок не гарантирован при ретраях, поэтому используйте paymentId, eventId и paymentSequence, а не время прихода запроса:
{
"event": "receipt.ready",
"schemaVersion": 1,
"eventId": "receipt.ready:19:14918505280:v1",
"paymentId": "14918505280",
"receiptNumber": "QR14918505280",
"type": "invoice",
"status": "Processed",
"paymentSequence": 2,
"paymentMethod": "kaspi_gold",
"paymentMethodRaw": "с Kaspi Gold",
"receipt": {
"fiscalLink": "https://receipt.kaspi.kz/web/fiscal?...",
"fiscalSign": "683156955461",
"registerNumber": "600704528354",
"amount": "4200.0",
"saleDate": "2026-07-15 16:03:33.179662"
},
"paidAt": "2026-07-15T11:03:35Z",
"receiptAvailableAt": "2026-07-15T11:03:36Z",
"timestamp": 1784113416
}
Для всех новых webhook-событий передаётся Idempotency-Key: <eventId>. У receipt.ready дополнительно есть X-Webhook-Sequence: 2. Один и тот же eventId может прийти повторно при сетевом ретрае — сохраняйте его с UNIQUE-ограничением. receipt.ready не заменяет payment.success и не должно повторно выдавать товар или доступ: используйте его только для сохранения способа оплаты и ссылки на фискальный чек.
Обязательная идемпотентность
Webhook-доставка имеет семантику at least once: один логический event может прийти повторно, даже если предыдущий запрос был фактически обработан. Например, ваш сервер успел выдать доступ, но ответ потерялся по таймауту, либо наш процесс перезапустился до фиксации полученного 2xx.
- Создайте таблицу входящих событий с
event_id UNIQUEилиPRIMARY KEY. - В одной транзакции вставляйте
eventIdи применяйте изменение бизнес-состояния. - На конфликте уникальности ничего повторно не выдавайте и отвечайте
2xx. - Для
payment.successдополнительно полезен UNIQUE поpaymentIdв таблице покупок/заказов. - Не дедуплицируйте все события только по
paymentId: у одного платежа отдельно приходятpayment.success,receipt.readyи несколькоpayment.refunded. - Не отклоняйте payload из-за незнакомых дополнительных полей — схема webhook расширяется обратно совместимыми полями.
Если выдача доступа происходит во внешней системе, используйте локальный outbox: в одной транзакции сохраните eventId и задачу на выдачу, затем отдельный worker идемпотентно выполнит эту задачу.
Проверка подписи
Заголовок X-Webhook-Signature = sha256= +
HMAC-SHA256 от сырого тела запроса, ключ — ваш secret.
Считайте подпись на своей стороне и сравнивайте константно (hmac.compare_digest).
import hmac, hashlib
def verify(body_bytes: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), body_bytes, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header)
2xx
завершает доставку. При таймауте, сетевой ошибке или ответе не 2xx
для каждого webhook URL предусмотрены четыре плановые попытки: первая сразу,
затем через 5 секунд, ещё через 30 секунд и ещё через 5 минут. После последней
плановой неудачи отправка прекращается. Доставка имеет семантику
at least once: если наш процесс упадёт между HTTP-запросом и фиксацией
его результата, одна из попыток может повториться и фактических запросов окажется
больше четырёх. Дедуплицируйте по eventId / Idempotency-Key
и отвечайте 2xx только после сохранения события.
Ошибки
| HTTP | Когда |
|---|---|
401 | Отсутствует или невалиден X-API-Key. |
400 | Неподдерживаемое событие вебхука, попытка возврата суммы больше AvailableReturnAmount, отмена уже завершённого счёта, небезопасный URL вебхука. |
404 | Вебхук не найден. |
412 | kaspi_session_required — сессия Kaspi отсутствует или была сброшена (например, вы залогинились в приложении Kaspi Pay на телефоне). Заново пройдите SMS-флоу. |
422 | Pydantic-валидация тела запроса (например, пустой items, price ≤ 0). |
502 | bad_gateway — Kaspi вернул ошибку. detail содержит {"kaspi": "...", "code": N}. |
500 | Внутренняя ошибка сервиса — администраторы уже получили алерт. |
Частые причины
- «kaspi_session_required» — вы или ваш сотрудник вошли в Kaspi Pay на смартфоне, и Kaspi выгнал нашу сессию. Зайдите в бот →
/login→ пройдите SMS заново. - Номер телефона неверен (
400 invalid_phone) — сервис принимает 10 или 11 цифр (с 8 или 7 в начале, можно с пробелами/скобками/плюсом). Если получили эту ошибку — в строке скорее всего меньше 10 цифр или есть лишние. - Клиент не получил push — у клиента не установлен Kaspi.kz или отключены уведомления. Сумма всё равно зачислится после оплаты вручную в приложении.
Пример интеграции (Python)
import os, requests
API = "https://pay.proverkacheka.kz/api"
KEY = os.environ["KASPI_API_KEY"]
H = {"X-API-Key": KEY, "Content-Type": "application/json"}
# 1. Выставить счёт
r = requests.post(f"{API}/v1/invoice/create", headers=H, json={
"phoneNumber": "87019009393",
"comment": "Заказ #42",
"items": [
{"name": "Капучино", "price": 1200, "count": 2},
{"name": "Чизкейк", "price": 1800, "count": 1},
],
})
r.raise_for_status()
payment_id = r.json()["paymentId"]
print("Счёт создан:", payment_id)
# 2. (Опционально) проверить статус — но лучше подписаться на вебхук
status = requests.get(f"{API}/v1/invoice/{payment_id}", headers=H).json()
print(status["status"])
# 3. Подписаться на вебхук один раз при старте бота/CRM
requests.post(f"{API}/v1/webhooks", headers=H, json={
"url": "https://my-bot.example.com/kaspi/webhook",
"events": ["payment.success", "payment.failed", "payment.expired", "payment.refunded"],
"secret": "long-random-shared-secret",
"label": "my-bot", # необязательно: тег, чтобы различать подписки
})
Пример приёма вебхука (Flask)
from flask import Flask, request, abort
import hashlib, hmac, json, os, sqlite3
SECRET = os.environ["KASPI_WEBHOOK_SECRET"].encode()
app = Flask(__name__)
def apply_once(event: dict) -> bool:
"""Атомарно дедуплицирует event и применяет локальный side effect."""
event_id = str(event.get("eventId") or "")
if not event_id:
raise ValueError("eventId is required")
with sqlite3.connect("app.db") as db:
db.execute("BEGIN IMMEDIATE")
seen = db.execute(
"SELECT 1 FROM webhook_events WHERE event_id = ?", (event_id,)
).fetchone()
if seen:
return False
db.execute(
"INSERT INTO webhook_events(event_id, event_name, payment_id) VALUES (?,?,?)",
(event_id, event["event"], event["paymentId"]),
)
if event["event"] == "payment.success":
# UNIQUE(payment_id) дополнительно защищает от двойной выдачи.
db.execute(
"INSERT INTO payment_state(payment_id, paid) VALUES (?,1) "
"ON CONFLICT(payment_id) DO UPDATE SET paid=1",
(event["paymentId"],),
)
elif event["event"] == "receipt.ready":
# receipt.ready может физически прийти раньше payment.success.
db.execute(
"INSERT INTO payment_state(payment_id, payment_method, fiscal_link) "
"VALUES (?,?,?) ON CONFLICT(payment_id) DO UPDATE SET "
"payment_method=excluded.payment_method, fiscal_link=excluded.fiscal_link",
(event["paymentId"], event.get("paymentMethod"),
(event.get("receipt") or {}).get("fiscalLink"),
),
)
return True
@app.post("/kaspi/webhook")
def webhook():
body = request.get_data()
sig = request.headers.get("X-Webhook-Signature", "")
expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, sig):
abort(401)
event = json.loads(body)
header_key = request.headers.get("Idempotency-Key")
if header_key and header_key != event.get("eventId"):
abort(400)
apply_once(event) # дубль вернёт False и не повторит side effect
return "", 200
Минимальная схема для примера:
CREATE TABLE webhook_events (
event_id TEXT PRIMARY KEY,
event_name TEXT NOT NULL,
payment_id TEXT NOT NULL,
received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE payment_state (
payment_id TEXT PRIMARY KEY,
paid INTEGER NOT NULL DEFAULT 0,
payment_method TEXT,
fiscal_link TEXT
);
Лимиты и особенности
| Параметр | Значение |
|---|---|
| Срок действия неоплаченного счёта | ~24 часа (контролируется Kaspi) |
| Позиций в корзине | 1–50 |
| Формат номера клиента | 10/11 цифр, любой казахстанский формат (нормализуется в 8XXXXXXXXXX) |
| Минимальная сумма позиции | > 0 ₸ |
| Активных счетов одновременно | без жёсткого лимита (поллим все) |
| Webhook retry | для всех новых событий: 4 плановые попытки на URL — сразу → +5 с → +30 с → +5 мин; возможны дубли при восстановлении после сбоя |
| Алгоритм подписи webhook | HMAC-SHA256 над сырым телом |
Краткая справка по эндпоинтам
| Метод | Путь | Описание |
|---|---|---|
| GET | /v1/auth/me | Информация о ключе и привязанной сессии Kaspi |
| POST | /v1/invoice/create | Создать счёт |
| GET | /v1/invoice/{id} | Статус счёта |
| GET | /v1/invoice | Активные счета |
| POST | /v1/invoice/{id}/cancel | Отменить ожидающий счёт |
| POST | /v1/invoice/{id}/refund | Возврат оплаченного счёта |
| GET | /v1/invoice/{id}/refunds | История возвратов |
| POST | /v1/webhooks | Подписаться на события |
| GET | /v1/webhooks | Список подписок |
| DELETE | /v1/webhooks/{id} | Удалить подписку |