API-справочник
REST поверх HTTPS, JSON в обе стороны, суммы — целые числа тенге, время — ISO 8601 в UTC. Базовый адрес: https://api.getyespay.com
Авторизация
Каждый запрос к /v1/* авторизуется секретным API-ключом. Ключ передаётся заголовком Authorization: Bearer или X-API-Key — обе формы равнозначны.
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 — человеческий текст, он может меняться.
{
"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.
| Группа | Лимит | Окно |
|---|---|---|
| Все запросы к /v1 | 200 | 1 минута |
| Создание платежей и счетов, отмена | 60 | 1 минута |
| Чтение платежей, счетов, возвратов | 600 | 1 минута |
| Создание возвратов | 20 | 1 минута |
Идемпотентность
Заголовок Idempotency-Key поддерживают все запросы, которые двигают деньги: POST /v1/payments, POST /v1/invoices, POST /v1/refunds. Ключ — произвольная строка до 255 символов, уникальная в рамках вашего мерчанта.
# Первый запрос — счёт создан.
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 и решайте по факту.
Платежи
Создать платёж
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: счёт доставлен покупателю, оплаты ещё нет.
{
"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— платёж создан тестовым ключом.
Прочитать платёж
Возвращает объект платежа. Чужой или несуществующий id — 404 resource_not_found.
Список платежей
- statusstring
- Фильтр по статусу, например paid.
- order_idstring
- Фильтр по вашему идентификатору заказа.
- limitinteger
- От 1 до 100, по умолчанию 25.
- starting_afterstring
- Курсор: id последнего элемента предыдущей страницы. Следующий курсор — в поле
next_cursor.
Ответ: { "object": "list", "data": [...], "has_more": bool, "next_cursor": string | null }. Сортировка — от новых к старым.
Отменить платёж
- Отменяется только неоплаченный счёт. Финальный статус даёт
409 payment_not_cancellable. - Уже оплаченный платёж отменить нельзя — нужен возврат, и API прямо об этом говорит.
- QR-платёж Kaspi отменить нельзя: придёт
501 provider_not_supported. Он гасится сам по истечении срока.
История статусов платежа
До 200 переходов в хронологическом порядке, вместе с сырым статусом провайдера. Это первое место, куда стоит смотреть при спорном платеже.
{
"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.
Выставить счёт
Тело — как у платежа, но без поля method (всегда remote_invoice). Ответ 201 содержит объект счёта и вложенный объект payment.
Прочитать счёт
Список счетов
Отменить счёт
Отмена счёта отменяет и связанный платёж; ограничения те же, что у POST /v1/payments/:id/cancel.
Возвраты
Вернуть деньги
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 символов. Хранится у нас и видна в кабинете.
{
"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.
Прочитать возврат
Список возвратов
Поддерживает фильтр payment_id и limit (1–100, по умолчанию 25).
Вебхуки
Вебхук — то, как ваш бэкенд узнаёт об оплате. Postgres у нас источник правды, очередь — только ускоритель: если очередь упала, доставка всё равно произойдёт.
Создать эндпоинт
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_…. Это единственный момент, когда его видно. Эндпоинт привязан к режиму ключа, которым создан: тестовые события не придут на боевой эндпоинт.
Список эндпоинтов
Изменить эндпоинт
Меняются url, events, enabled, description.
Удалить эндпоинт
Отправить тестовое событие
Присылает событие webhook.test на все включённые эндпоинты текущего режима или на один, если передать { "webhook_id": "wh_…" }. Отвечает 202. Нужен, чтобы проверить свою проверку подписи до того, как от неё будет зависеть реальный платёж.
Журнал доставок
Статус каждой доставки, число попыток, HTTP-код вашего ответа, время следующей попытки и последняя ошибка. Фильтры: webhook_id, limit.
Список типов событий
Как выглядит доставка
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.
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 по уже истёкшему платежу, статус не переписывается — заводится критический алерт и решает человек.
Коды ошибок
| HTTP | code | Что делать |
|---|---|---|
| 401 | missing_api_key | Заголовок авторизации не передан. |
| 401 | invalid_api_key | Ключ неверен, повреждён или это публикуемый ключ вместо секретного. |
| 401 | api_key_revoked | Ключ отозван. Выпустите новый в кабинете. |
| 403 | forbidden | Операция запрещена для этого ключа (например, тестовый ключ создаёт боевой). |
| 403 | merchant_suspended | Аккаунт приостановлен. Напишите в поддержку. |
| 404 | resource_not_found | Объекта нет либо он принадлежит другому мерчанту или другому режиму. |
| 409 | idempotency_key_reused | Тот же ключ с другим телом запроса. Возьмите новый ключ. |
| 409 | idempotency_request_in_flight | Предыдущий запрос с этим ключом ещё выполняется. Повторите через секунду. |
| 409 | payment_not_cancellable | Платёж уже финальный или оплачен — нужен возврат. |
| 409 | payment_not_refundable | Возврат возможен только для оплаченного платежа. |
| 409 | refund_amount_exceeded | Сумма больше остатка. Остаток — в details.remaining. |
| 409 | payment_account_missing | Кассир Kaspi не подключён. Подключите его в кабинете. |
| 409 | payment_account_reauth_required | Сессия кассира вытеснена. Пройдите подключение заново. |
| 409 | payment_account_blocked | Kaspi заблокировал доступ этому сотруднику. |
| 422 | validation_failed | Тело не прошло проверку. Разбор по полям — в details.fields. |
| 422 | invalid_phone | Номер не похож на казахстанский мобильный. |
| 422 | invalid_amount | Сумма дробная, меньше 1 или больше 10 000 000. |
| 422 | unsupported_currency | Поддерживается только KZT. |
| 429 | rate_limited | Слишком часто. Ждите Retry-After секунд. |
| 501 | provider_not_supported | Операция недоступна у провайдера — например, частичный возврат или отмена QR. |
| 502 | provider_rejected | Kaspi отклонил операцию. Текст причины — в message. |
| 503 | provider_unavailable | Kaspi недоступен либо у сервера сбиты часы. Повторите с выдержкой. |
| 503 | provider_session_evicted | Сессию кассира вытеснили во время запроса. Требуется повторное подключение. |
| 500 | internal_error | Наша ошибка. Приложите request_id из ответа. |
Что безопасно повторять
Повторяйте 429, 502, 503 и сетевые таймауты — с экспоненциальной выдержкой и тем же Idempotency-Key. Ошибки 4xx, кроме 409 idempotency_request_in_flight, повторять бессмысленно: сначала исправьте запрос.
API-ключи
Список ключей
Секретные значения не возвращаются — только превью вида sk_live_abc…4f2a.
Создать ключ
Принимает name и mode (test по умолчанию). Тестовым ключом нельзя создать боевой. Секрет возвращается один раз.
Управляйте ключами из кабинета
Ключ, умеющий выпускать другие ключи, означает, что утёкший ключ даёт закрепление: злоумышленник выпустит второй, и отзыв первого его не выкинет. Эндпоинт существует, каждое действие пишется в аудит, но повседневно безопаснее выпускать и отзывать ключи в кабинете.
Дальше
Инструкция по подключению — «Как подключить yespay». Готовые промты для ИИ-агентов — /docs/agents.
