API-справочник

REST поверх HTTPS, JSON в обе стороны, суммы — целые числа тенге, время — ISO 8601 в UTC. Базовый адрес: https://api.getyespay.com

Авторизация

Каждый запрос к /v1/* авторизуется секретным API-ключом. Ключ передаётся заголовком Authorization: Bearer или X-API-Key — обе формы равнозначны.

bash
curl https://api.getyespay.com/v1/payments \
  -H "Authorization: Bearer sk_live_..."

# Эквивалентная форма:
curl https://api.getyespay.com/v1/payments -H "X-API-Key: sk_live_..."
  • Ключ сам определяет режим: sk_live_… работает с настоящим Kaspi, sk_test_… — с мок-провайдером. Данные режимов полностью изолированы: тестовым ключом нельзя увидеть боевой платёж.
  • Публикуемый ключ (pk_…) на /v1/* не принимается — в ответ придёт invalid_api_key с явным указанием, что нужен секретный ключ.
  • Сессия кабинета не даёт доступа к /v1/*, а API-ключ не даёт доступа к /dashboard/*. Это разделение намеренное: XSS в кабинете не должен превращаться в возможность выставлять счета.

Формат ошибки

У всех ошибок одна форма. Переключайтесь по code: он стабилен. message — человеческий текст, он может меняться.

json
{
  "error": {
    "code": "payment_not_refundable",
    "message": "A payment in state \"pending\" cannot be refunded.",
    "type": "conflict",
    "details": { "status": "pending" }
  }
}

В каждом ответе есть заголовок X-Request-Id. Это то, что нужно приложить к обращению в поддержку.

Лимиты запросов

Лимиты считаются скользящим окном на каждый API-ключ. В ответе — заголовки X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset; при превышении приходит 429 с кодом rate_limited и заголовком Retry-After.

ГруппаЛимитОкно
Все запросы к /v12001 минута
Создание платежей и счетов, отмена601 минута
Чтение платежей, счетов, возвратов6001 минута
Создание возвратов201 минута

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

Заголовок Idempotency-Key поддерживают все запросы, которые двигают деньги: POST /v1/payments, POST /v1/invoices, POST /v1/refunds. Ключ — произвольная строка до 255 символов, уникальная в рамках вашего мерчанта.

bash
# Первый запрос — счёт создан.
curl -X POST https://api.getyespay.com/v1/payments \
  -H "Authorization: Bearer sk_live_..." \
  -H "Idempotency-Key: ORDER-1001" \
  -H "Content-Type: application/json" \
  -d '{"amount":14990,"customer":{"phone":"+77001234567"},"order_id":"ORDER-1001"}'

# Тот же запрос ещё раз — тот же платёж, второго счёта нет.
# Ответ содержит заголовок: Idempotent-Replay: true
СитуацияЧто вернёт API
Ключ виден впервыеЗапрос выполняется обычным образом; результат сохраняется под этим ключом.
Тот же ключ, запрос уже завершёнВозвращается сохранённый ответ с заголовком Idempotent-Replay: true. Второй счёт не создаётся.
Тот же ключ, первый запрос ещё в работе409 idempotency_request_in_flight — повторите через секунду.
Тот же ключ, но другое тело запроса409 idempotency_key_reused. Один ключ описывает одну операцию.

Почему ключ не освобождается после ошибки провайдера

Если запрос упал до обращения к Kaspi (валидация, авторизация, лимит, кассир не подключён), ключ освобождается и его можно использовать снова. Если ошибка произошла там, где счёт в Kaspi мог уже быть создан, ключ остаётся занят: освободить его — значит пригласить клиента повторить запрос и выставить покупателю второй счёт. В этом случае прочитайте платёж по order_id и решайте по факту.

Платежи

POST/v1/payments

Создать платёж

bash
curl -X POST https://api.getyespay.com/v1/payments \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ORDER-1001" \
  -d '{
    "amount": 14990,
    "currency": "KZT",
    "customer": { "phone": "+77001234567" },
    "description": "Заказ №1001",
    "order_id": "ORDER-1001",
    "method": "remote_invoice",
    "metadata": { "cart_id": "c_42" }
  }'
amountinteger, обязательно
Сумма целым числом тенге. Минимум 1, максимум 10 000 000. Дробные значения отклоняются.
currencystring
Только KZT (значение по умолчанию).
customer.phonestring, обязательно
Номер покупателя. Принимается в любом разумном виде (+7…, 8…, 10 цифр) и нормализуется в E.164.
descriptionstring
Назначение платежа, до 255 символов.
order_idstring
Ваш идентификатор заказа, до 128 символов. По нему потом удобно искать: GET /v1/payments?order_id=…
methodstring
remote_invoice (по умолчанию) — счёт прилетает покупателю в приложение Kaspi. qr — платёж по QR-коду.
metadataobject
Произвольный JSON, возвращается как есть в платеже и в вебхуках.

Ответ — 201 и объект платежа. Статус в момент создания — pending: счёт доставлен покупателю, оплаты ещё нет.

json
{
  "id": "pay_3Nq8xVb2...",
  "object": "payment",
  "status": "paid",
  "amount": 14990,
  "currency": "KZT",
  "order_id": "ORDER-1001",
  "description": "Заказ №1001",
  "customer": { "phone": "+77001234567" },
  "provider": "kaspi",
  "method": "remote_invoice",
  "receipt_url": "https://kaspi.kz/pay/receipt/...",
  "qr": null,
  "livemode": true,
  "refunded_amount": 0,
  "failure_code": null,
  "failure_message": null,
  "expires_at": "2026-09-07T09:00:00.000Z",
  "paid_at": "2026-09-06T09:01:12.000Z",
  "created_at": "2026-09-06T09:00:00.000Z",
  "metadata": { "cart_id": "c_42" }
}
receipt_urlstring | null
Ссылка на чек Kaspi. Появляется вместе с оплатой, до этого null.
qrobject | null
Для method: "qr": url — универсальный QR (его платят и приложения других банков), kaspi_app_url — прямая ссылка в приложение Kaspi.
refunded_amountinteger
Сколько уже возвращено по этому платежу, в тенге.
failure_code / failure_messagestring | null
Заполняются, когда платёж перешёл в failed.
provider_statusstring
Присутствует только при статусе unknown — сырой статус, который вернул Kaspi. Нужен поддержке для разбора.
livemodeboolean
false — платёж создан тестовым ключом.
GET/v1/payments/:id

Прочитать платёж

Возвращает объект платежа. Чужой или несуществующий id — 404 resource_not_found.

GET/v1/payments

Список платежей

statusstring
Фильтр по статусу, например paid.
order_idstring
Фильтр по вашему идентификатору заказа.
limitinteger
От 1 до 100, по умолчанию 25.
starting_afterstring
Курсор: id последнего элемента предыдущей страницы. Следующий курсор — в поле next_cursor.

Ответ: { "object": "list", "data": [...], "has_more": bool, "next_cursor": string | null }. Сортировка — от новых к старым.

POST/v1/payments/:id/cancel

Отменить платёж

  • Отменяется только неоплаченный счёт. Финальный статус даёт 409 payment_not_cancellable.
  • Уже оплаченный платёж отменить нельзя — нужен возврат, и API прямо об этом говорит.
  • QR-платёж Kaspi отменить нельзя: придёт 501 provider_not_supported. Он гасится сам по истечении срока.
GET/v1/payments/:id/events

История статусов платежа

До 200 переходов в хронологическом порядке, вместе с сырым статусом провайдера. Это первое место, куда стоит смотреть при спорном платеже.

json
{
  "object": "list",
  "data": [
    { "id": "pev_1...", "from": null,      "to": "created", "reason": "created",           "provider_status": null,                  "created_at": "2026-09-06T09:00:00.000Z" },
    { "id": "pev_2...", "from": "created", "to": "pending", "reason": "provider_accepted", "provider_status": "RemotePaymentCreated", "created_at": "2026-09-06T09:00:01.000Z" },
    { "id": "pev_3...", "from": "pending", "to": "paid",    "reason": "provider_status",   "provider_status": "Processed",            "created_at": "2026-09-06T09:01:12.000Z" }
  ]
}

Счета

Счёт — документ для мерчанта, платёж — движение денег. Создание счёта создаёт и платёж: у Kaspi нет понятия «неотправленный счёт», поэтому черновик был бы обещанием, которого мы не сможем выполнить. Если вам не нужен отдельный документ, работайте напрямую с /v1/payments.

POST/v1/invoices

Выставить счёт

Тело — как у платежа, но без поля method (всегда remote_invoice). Ответ 201 содержит объект счёта и вложенный объект payment.

GET/v1/invoices/:id

Прочитать счёт

GET/v1/invoices

Список счетов

POST/v1/invoices/:id/cancel

Отменить счёт

Отмена счёта отменяет и связанный платёж; ограничения те же, что у POST /v1/payments/:id/cancel.

Возвраты

POST/v1/refunds

Вернуть деньги

bash
curl -X POST https://api.getyespay.com/v1/refunds \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: REFUND-ORDER-1001" \
  -d '{ "payment_id": "pay_3Nq8xVb2...", "reason": "Покупатель отказался" }'
payment_idstring, обязательно
Платёж в статусе paid или partially_refunded.
amountinteger
Опустите для полного возврата остатка. См. предупреждение ниже.
reasonstring
Причина, до 255 символов. Хранится у нас и видна в кабинете.
json
{
  "id": "ref_8Km2...",
  "object": "refund",
  "payment_id": "pay_3Nq8xVb2...",
  "status": "succeeded",
  "amount": 14990,
  "currency": "KZT",
  "reason": "Покупатель отказался",
  "failure_code": null,
  "failure_message": null,
  "created_at": "2026-09-06T10:00:00.000Z",
  "succeeded_at": "2026-09-06T10:00:02.000Z"
}

Частичный возврат сейчас недоступен

Возврат части суммы через Kaspi нами не подтверждён живым прогоном, поэтому запрос с amount меньше суммы платежа возвращает 501 provider_not_supported. Мы предпочитаем честный отказ имитации возврата, который мы не можем проверить. Полный возврат работает и проверен на реальном платеже.

  • Возврат синхронный: как правило, ответ уже содержит status: "succeeded".
  • Сумма больше остатка — 409 refund_amount_exceeded с полем remaining в деталях.
  • Платёж не в оплаченном состоянии — 409 payment_not_refundable.
GET/v1/refunds/:id

Прочитать возврат

GET/v1/refunds

Список возвратов

Поддерживает фильтр payment_id и limit (1–100, по умолчанию 25).

Вебхуки

Вебхук — то, как ваш бэкенд узнаёт об оплате. Postgres у нас источник правды, очередь — только ускоритель: если очередь упала, доставка всё равно произойдёт.

POST/v1/webhooks

Создать эндпоинт

bash
curl -X POST https://api.getyespay.com/v1/webhooks \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://shop.kz/webhooks/yespay",
    "events": ["payment.paid", "payment.failed", "payment.status_unknown"],
    "description": "Прод, основной обработчик"
  }'
urlstring, обязательно
Публичный https:// адрес. Приватные и служебные адреса отклоняются — и здесь, и на каждой доставке.
eventsstring[]
Список типов событий. Пустой список или отсутствие поля — присылать всё.
descriptionstring
Заметка для вашего же кабинета.

Секрет возвращается один раз

В ответе 201 есть поле secret вида whsec_…. Это единственный момент, когда его видно. Эндпоинт привязан к режиму ключа, которым создан: тестовые события не придут на боевой эндпоинт.

GET/v1/webhooks

Список эндпоинтов

PATCH/v1/webhooks/:id

Изменить эндпоинт

Меняются url, events, enabled, description.

DELETE/v1/webhooks/:id

Удалить эндпоинт

POST/v1/webhooks/test

Отправить тестовое событие

Присылает событие webhook.test на все включённые эндпоинты текущего режима или на один, если передать { "webhook_id": "wh_…" }. Отвечает 202. Нужен, чтобы проверить свою проверку подписи до того, как от неё будет зависеть реальный платёж.

GET/v1/webhooks/deliveries

Журнал доставок

Статус каждой доставки, число попыток, HTTP-код вашего ответа, время следующей попытки и последняя ошибка. Фильтры: webhook_id, limit.

GET/v1/webhooks/event-types

Список типов событий

Как выглядит доставка

http
POST /webhooks/yespay HTTP/1.1
Content-Type: application/json
User-Agent: yespay-webhooks/1.0
X-Yespay-Signature: t=1757148037,v1=5f2c...c91
X-Yespay-Event-Id: evt_9F2K...
X-Yespay-Delivery-Id: whd_4Tz...
X-Yespay-Attempt: 1

{
  "id": "evt_9F2K...",
  "type": "payment.paid",
  "createdAt": "2026-09-06T09:01:12.000Z",
  "livemode": true,
  "data": {
    "id": "pay_3Nq8xVb2...",
    "object": "payment",
    "status": "paid",
    "amount": 14990,
    "currency": "KZT",
    "order_id": "ORDER-1001"
  }
}
X-Yespay-Signatureheader
Подпись вида t=<unix>,v1=<hmac>.
X-Yespay-Event-Idheader
Идентификатор события. Стабилен между попытками — дедуплицируйте по нему.
X-Yespay-Delivery-Idheader
Идентификатор конкретной доставки на конкретный эндпоинт.
X-Yespay-Attemptheader
Номер попытки, начиная с 1.
createdAtstring
Время события, ISO 8601. Обратите внимание: в теле события это поле в camelCase, а внутри data — обычный snake_case объекта платежа.

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

Подписываемая строка — {timestamp}.{сырое тело}, алгоритм HMAC-SHA256, ключ — секрет эндпоинта, результат — hex.

python
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.strip().split("=", 1) for p in header.split(","))
    timestamp = int(parts["t"])
    if abs(time.time() - timestamp) > tolerance:
        return False                      # слишком старое событие
    expected = hmac.new(
        secret.encode(),
        f"{timestamp}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
  • Считайте подпись по сырым байтам тела. Разбор и повторная сериализация JSON меняют байты и ломают проверку.
  • Отклоняйте события старше 5 минут: подпись без проверки времени не защищает от переигрывания.
  • Сравнивайте в постоянном времени (hmac.compare_digest, crypto.timingSafeEqual).

Повторы

Доставка считается успешной при ответе 2xx в течение 10 секунд. Иначе — до 6 попыток с растущей задержкой, примерно от 5 секунд до часа. Редиректы не выполняются: ответ 3xx считается неудачей.

Типы событий

СобытиеКогда приходит
payment.createdПлатёж создан у нас, до ответа провайдера.
payment.pendingСчёт принят Kaspi и ждёт покупателя.
payment.paidОплачено. Это единственное событие, по которому можно отгружать заказ.
payment.failedОплата не прошла.
payment.expiredСчёт истёк неоплаченным.
payment.cancelledСчёт отменён вами.
payment.status_unknownМы не смогли определить исход. Не отказ: заказ надо придержать, платёж уже поднял алерт у нас.
refund.createdВозврат создан.
refund.succeededДеньги возвращены.
refund.failedВозврат не прошёл.
payment_account.connectedКассир Kaspi подключён.
payment_account.reauth_requiredСессия кассира больше не действует: новые платежи создаваться не будут, нужно повторить подключение.
webhook.testТолько по вашему запросу через POST /v1/webhooks/test.

Статусы платежа

СтатусФинальныйЧто значит
createdнетПлатёж заведён у нас, счёт в Kaspi ещё не выставлен.
pendingнетСчёт у покупателя, оплаты нет.
processingнетKaspi обрабатывает оплату.
paidдаОплачено. Деньги на вашем счёте Kaspi Business.
failedдаОплата не прошла.
expiredдаСрок счёта истёк.
cancelledдаОтменён до оплаты.
refund_pendingнетВозврат запущен.
partially_refundedнетВозвращена часть суммы.
refundedдаВозвращена вся сумма.
unknownнетПровайдер вернул статус, который мы не распознаём. Исход неизвестен.

unknown — это не failed

Мы никогда не превращаем нераспознанный статус в отказ и никогда не угадываем исход по подстроке в ответе провайдера. Платёж остаётся в unknown, у нас поднимается критический алерт, а вам приходит payment.status_unknown с полем provider_status. Ваша интеграция должна придержать заказ, а не отменять его.

По той же причине мы не применяем молча противоречащие переходы: если провайдер сообщает paid по уже истёкшему платежу, статус не переписывается — заводится критический алерт и решает человек.

Коды ошибок

HTTPcodeЧто делать
401missing_api_keyЗаголовок авторизации не передан.
401invalid_api_keyКлюч неверен, повреждён или это публикуемый ключ вместо секретного.
401api_key_revokedКлюч отозван. Выпустите новый в кабинете.
403forbiddenОперация запрещена для этого ключа (например, тестовый ключ создаёт боевой).
403merchant_suspendedАккаунт приостановлен. Напишите в поддержку.
404resource_not_foundОбъекта нет либо он принадлежит другому мерчанту или другому режиму.
409idempotency_key_reusedТот же ключ с другим телом запроса. Возьмите новый ключ.
409idempotency_request_in_flightПредыдущий запрос с этим ключом ещё выполняется. Повторите через секунду.
409payment_not_cancellableПлатёж уже финальный или оплачен — нужен возврат.
409payment_not_refundableВозврат возможен только для оплаченного платежа.
409refund_amount_exceededСумма больше остатка. Остаток — в details.remaining.
409payment_account_missingКассир Kaspi не подключён. Подключите его в кабинете.
409payment_account_reauth_requiredСессия кассира вытеснена. Пройдите подключение заново.
409payment_account_blockedKaspi заблокировал доступ этому сотруднику.
422validation_failedТело не прошло проверку. Разбор по полям — в details.fields.
422invalid_phoneНомер не похож на казахстанский мобильный.
422invalid_amountСумма дробная, меньше 1 или больше 10 000 000.
422unsupported_currencyПоддерживается только KZT.
429rate_limitedСлишком часто. Ждите Retry-After секунд.
501provider_not_supportedОперация недоступна у провайдера — например, частичный возврат или отмена QR.
502provider_rejectedKaspi отклонил операцию. Текст причины — в message.
503provider_unavailableKaspi недоступен либо у сервера сбиты часы. Повторите с выдержкой.
503provider_session_evictedСессию кассира вытеснили во время запроса. Требуется повторное подключение.
500internal_errorНаша ошибка. Приложите request_id из ответа.

Что безопасно повторять

Повторяйте 429, 502, 503 и сетевые таймауты — с экспоненциальной выдержкой и тем же Idempotency-Key. Ошибки 4xx, кроме 409 idempotency_request_in_flight, повторять бессмысленно: сначала исправьте запрос.

API-ключи

GET/v1/api-keys

Список ключей

Секретные значения не возвращаются — только превью вида sk_live_abc…4f2a.

POST/v1/api-keys

Создать ключ

Принимает name и mode (test по умолчанию). Тестовым ключом нельзя создать боевой. Секрет возвращается один раз.

Управляйте ключами из кабинета

Ключ, умеющий выпускать другие ключи, означает, что утёкший ключ даёт закрепление: злоумышленник выпустит второй, и отзыв первого его не выкинет. Эндпоинт существует, каждое действие пишется в аудит, но повседневно безопаснее выпускать и отзывать ключи в кабинете.

Дальше

Инструкция по подключению — «Как подключить yespay». Готовые промты для ИИ-агентов — /docs/agents.