Главная › Документация API

Reference · v1

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

Полный технический референс Kaspi Pay invoice API: авторизация по SMS, создание счёта, статусы, отмена, возвраты, вебхуки с X-Webhook-Signature: sha256=..., коды ошибок.

Получить API-ключ → Открыть .md Fallback: invoice + PDF → Гайды и статьи Swagger UI Health

Отказ от ответственности

Сервис PayProverkaBot (pay.proverkacheka.kz) не связан и не аффилирован с АО «Kaspi Bank» или любыми другими организациями группы Kaspi. Мы не являемся платёжным сервисом, эквайрингом или финансовой организацией. Мы не являемся партнёрами, представителями или агентами Kaspi.

PayProverkaBot предоставляет техническую услугу — программный интерфейс к функции «удалённый счёт» приложения Kaspi Pay для бизнеса от имени самого мерчанта, после его добровольной авторизации по SMS на его собственный номер.

Ответственность пользователя

Используя сервис, вы подтверждаете, что:

  1. Вы являетесь владельцем (или уполномоченным представителем) аккаунта Kaspi Pay для бизнеса, по которому проходит авторизация.
  2. Вы соблюдаете законодательство Республики Казахстан и условия Kaspi.
  3. Вы используете сервис в законных целях — автоматизация выставления счетов, интеграция с CRM, чат-ботами, кассовыми системами.
  4. Вы не используете сервис для незаконной деятельности, отмывания денег, уклонения от налогов или иных противоправных действий.

Администрация PayProverkaBot не несёт ответственности за прямые или косвенные убытки, возникшие в результате использования сервиса, блокировки аккаунта Kaspi или технических сбоев на стороне Kaspi.

Начало работы

Как получить API-ключ

  1. Откройте @PayProverkaBot в Telegram.
  2. Нажмите /start — получите персональный API_KEY.
  3. Авторизуйтесь в Kaspi Pay: /login → отправьте номер телефона ИП/ТОО → введите SMS-код.
  4. Готово — можете выставлять счета через бота (/invoice 87019009393 1500 За кофе) или через REST API.
Совет. Команда /apikey в боте покажет ваш ключ повторно. Команда /account покажет, к какому аккаунту Kaspi привязана сессия.

Базовый URL и заголовки

Что делает сервис

Проверка 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 requiredstringНомер клиента. Сервис автоматически нормализует — принимаются форматы 87019009393, 77019009393, 7019009393, +7 701 900 93 93. На вход в Kaspi уходит каноничный 8XXXXXXXXXX. На невалидный формат — 400 invalid_phone.
items requiredarray1–50 позиций корзины. См. ниже.
comment optionalstringОписание счёта. По умолчанию — названия позиций через запятую.
source optionalstring (≤50)Пространство имён интеграции, например kommo, tilda или merchant_crm.
externalUserId optionalstring (≤128)ID клиента в системе интегратора. Не заменяет телефон и не влияет на доставку счёта.
externalUserType optionalstring (≤50)Тип ID: telegram, kommo_contact, crm_customer и т.п. Используйте вместе с source.
externalDealId optionalstring (≤100)ID сделки/заказа. Для kommo.leadId заполняется автоматически.
customerTgUserId optionalintegerСокращённый вариант: нормализуется в externalUserId + externalUserType="telegram".

Все поля внешней идентификации необязательны. Старые клиенты с phoneNumber, items и comment продолжают работать без изменений.

Структура items[i]

ПолеТипОписание
name requiredstring (1–200)Название товара/услуги — показывается в чеке клиента.
price requirednumber > 0Цена за единицу в тенге.
count optionalinteger > 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 optionalnumber > 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.

Если выдача доступа происходит во внешней системе, используйте локальный 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)
Retry-политика webhook. Любой ответ 2xx завершает доставку. При таймауте, сетевой ошибке или ответе не 2xx для каждого webhook URL предусмотрены четыре плановые попытки: первая сразу, затем через 5 секунд, ещё через 30 секунд и ещё через 5 минут. После последней плановой неудачи отправка прекращается. Доставка имеет семантику at least once: если наш процесс упадёт между HTTP-запросом и фиксацией его результата, одна из попыток может повториться и фактических запросов окажется больше четырёх. Дедуплицируйте по eventId / Idempotency-Key и отвечайте 2xx только после сохранения события.

Ошибки

HTTPКогда
401Отсутствует или невалиден X-API-Key.
400Неподдерживаемое событие вебхука, попытка возврата суммы больше AvailableReturnAmount, отмена уже завершённого счёта, небезопасный URL вебхука.
404Вебхук не найден.
412kaspi_session_required — сессия Kaspi отсутствует или была сброшена (например, вы залогинились в приложении Kaspi Pay на телефоне). Заново пройдите SMS-флоу.
422Pydantic-валидация тела запроса (например, пустой items, price ≤ 0).
502bad_gateway — Kaspi вернул ошибку. detail содержит {"kaspi": "...", "code": N}.
500Внутренняя ошибка сервиса — администраторы уже получили алерт.

Частые причины

Пример интеграции (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 мин; возможны дубли при восстановлении после сбоя
Алгоритм подписи webhookHMAC-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}Удалить подписку