С чего начать
AgentPay даёт три способа принимать оплату через Kaspi: веб-интерфейс, REST API и CLI. Веб работает сразу после регистрации. Для API и CLI:
- Создай ключ в
/app/settings/api-keys. Для разработки —sk_test_*(песочница, деньги не списываются, кассир Kaspi не нужен). Для боя —sk_live_*и подключённый кассир в /app/cashier. - Сделай первый запрос:bash
curl -X POST https://agentpay.kz/api/v1/invoices \ -H "Authorization: Bearer sk_test_..." \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "client_phone": "77001234567", "amount_kzt": 1500, "comment": "Бронь #123", "external_id": "BK-2026-000123", "metadata": { "room": "12A", "guests": 2 } }'Ответ — JSON с полем
pay_link_url: ссылка для клиента, открывает Kaspi в один тап (отправляй её в WhatsApp, SMS, чат). Параллельно Kaspi отправит push на телефон клиента.external_idиmetadataвернутся в ресурсе и в каждом вебхуке по этому счёту. - Подключи webhook endpoint в /app/settings/webhooks и жди
invoice.paid. В песочнице оплату можно симулировать:POST /v1/test/invoices/{id}/pay.
Аутентификация
Все запросы к https://agentpay.kz/api/v1/* требуют заголовок Authorization: Bearer <key>. Ключи делятся на:
sk_live_*— секретный ключ, боевой режим, реальные платежи через кассира Kaspi.sk_test_*— секретный ключ, песочница: счета и ссылки не уходят в Kaspi, деньги не списываются, кассир не нужен. См. раздел «Тестовый режим».pk_live_*/pk_test_*— публикуемые ключи для клиентских SDK. Сейчас не используются API-эндпоинтами (на сервере нужен толькоsk_*).
Ресурсы разделены по режимам: тестовый ключ видит только тестовые счета, ссылки и события, боевой — только боевые. У каждого ресурса и события есть поле mode: "live" | "test".
Ключи хранятся как HMAC-SHA256 хеши, raw-значение показывается ОДИН раз при создании. Сравнение constant-time.
Тестовый режим (sandbox)
Ключ sk_test_* включает песочницу. Всё API работает как в бою, кроме одного: Kaspi не вызывается.
POST /v1/invoicesиPOST /v1/payment_linksсразу возвращаютstatus: "success",mode: "test"иkaspi_invoice_idвидаtest_…. Push клиенту не уходит, кассир Kaspi не нужен.- Оплату, просрочку и возврат ты вызываешь сам — при этом уходят те же события и подписанные вебхуки, что и в бою:
# счёт оплачен → invoice.paid + вебхук
curl -X POST https://agentpay.kz/api/v1/test/invoices/{id}/pay -H "Authorization: Bearer sk_test_..."
# счёт просрочен → invoice.expired + вебхук
curl -X POST https://agentpay.kz/api/v1/test/invoices/{id}/expire -H "Authorization: Bearer sk_test_..."
# возврат и отмена — обычными эндпоинтами
curl -X POST https://agentpay.kz/api/v1/invoices/{id}/refunds -H "Authorization: Bearer sk_test_..." -H "Idempotency-Key: $(uuidgen)"
curl -X POST https://agentpay.kz/api/v1/invoices/{id}/cancel -H "Authorization: Bearer sk_test_..." -H "Idempotency-Key: $(uuidgen)"{id} — id счёта или платёжной ссылки. Повторный вызов на уже оплаченном счёте вернёт 200 с ресурсом (идемпотентно); на закрытом (отменён/просрочен) — 409 invalid_state. Боевой ключ на этих эндпоинтах получает 403 test_mode_required.
pay_link_url/pay_page_urlтестового счёта открывает hosted-страницу с плашкой «Тестовый режим» и кнопкой «Симулировать оплату» — удобно для ручной проверки WhatsApp-сценария.- Неоплаченные тестовые счета автоматически просрочиваются через 24 часа, как боевые.
- В CLI:
agentpay test pay <id>,agentpay test expire <id>. - Webhook endpoint один на аккаунт и получает события обоих режимов — проверяй поле
modeв теле события.
Rate limit
Лимиты на ключ, окно скользящее, минутное:
- 60 запросов в минуту — запись (POST: создание счетов и ссылок, cancel, refund, refresh, resend).
- 120 запросов в минуту — чтение (GET: списки, отдельные ресурсы, события).
- SSE (
/v1/events/stream) считается отдельно: 1 одновременное соединение на ключ, максимум 100 глобально.
Каждый ответ несёт заголовки:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 53
X-RateLimit-Reset: 1735689600На превышении — 429 Too Many Requests с Retry-After в секундах.
Идемпотентность
POST-эндпоинты, создающие или мутирующие ресурсы, требуют заголовок Idempotency-Key. Сгенерируй UUID на стороне клиента — повторный запрос с тем же ключом и тем же телом вернёт закешированный ответ (24 часа).
curl -X POST https://agentpay.kz/api/v1/invoices \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: 8e9d2b4a-1f5c-4a3d-9b8a-c7e1f0d3a5b6" \
-H "Content-Type: application/json" \
-d '{"client_phone":"77001234567","amount_kzt":1500}'Если повторить с тем же ключом, но другим телом — вернётся 409 idempotency_conflict: один ключ = одна операция. Практичный приём: выводи ключ из своего booking_id (например UUID v5 от строки booking:123) — тогда ретрай из любой части системы гарантированно не создаст второй счёт.
Счета
/v1/invoicesСоздать разовый счёт: Kaspi шлёт push на телефон клиента, плюс ты получаешь pay_link_url для отправки любым каналом. Idempotency-Key обязателен.
Тело:
{
"client_phone": "77001234567",
"amount_kzt": 1500,
"comment": "Бронь #123",
"external_id": "BK-2026-000123",
"metadata": { "room": "12A", "guests": 2, "source": "site" }
}| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
| client_phone | string | да | 11 цифр, начинается с 7, без + |
| amount_kzt | integer | да | Сумма в тенге, 1…500 000 |
| comment | string ≤200 | нет | Назначение. Виден плательщику в Kaspi |
| external_id | string ≤128 | нет | Твой id (booking_id / order_id). Не виден плательщику, возвращается в ресурсе и вебхуках, доступен как фильтр |
| metadata | object | нет | До 20 ключей (≤40 символов), значения string ≤500 / number / boolean / null. Не виден плательщику, возвращается в ресурсе и вебхуках |
Если у тенанта нет активного кассира (боевой режим) — 409 no_active_cashier.
/v1/invoicesСписок счетов, новые первыми. Cursor-пагинация, фильтры:
GET /v1/invoices?limit=20&status=paid&kind=one_time
GET /v1/invoices?external_id=BK-2026-000123
GET /v1/invoices?status=paid&paid_after=2026-09-01T00:00:00Z
GET /v1/invoices?created_after=2026-09-01T00:00:00Z
GET /v1/invoices?cursor=eyJjcmVhdGVkX2F0Ijoi...paid_after / created_after — ISO-8601, строго после указанного момента. Это основа инкрементальной сверки (см. «Обмен с 1С»).
/v1/invoices/{id}Один счёт по UUID. 404 если не найден, принадлежит другому тенанту или другому режиму.
/v1/invoices/{id}/cancelОтменить неоплаченный счёт. Тело пустое. Idempotency-Key обязателен.
/v1/invoices/{id}/refundsВозврат оплаченного счёта. Возвращается полная сумма. Idempotency-Key обязателен.
Invoice resource
{
"id": "8e9d2b4a-1f5c-4a3d-9b8a-c7e1f0d3a5b6",
"object": "invoice",
"kind": "one_time",
"status": "success",
"mode": "test",
"client_phone": "77001234567",
"amount_kzt": 1500,
"comment": "Бронь #123",
"external_id": "BK-2026-000123",
"metadata": { "room": "12A", "guests": 2, "source": "site" },
"kaspi_invoice_id": "1234567",
"receipt_url": "https://kaspi.kz/receipt/1234567",
"pay_link_url": "https://agentpay.kz/pay/Py1L9FpqkTvg...",
"created_at": "2026-09-07T18:27:45.752Z",
"ran_at": "2026-09-07T18:27:45.771Z",
"paid_at": null,
"expired_at": null,
"cancelled_at": null,
"refunded_at": null,
"subscription_id": null,
"error": null
}Три идентификатора у каждого платежа: id (наш UUID — ключ дедупликации на твоей стороне), kaspi_invoice_id (номер операции в Kaspi, в тесте test_…) и receipt_url (чек Kaspi, появляется после оплаты).
Статусы
Happy path: pending → success (отправлен в Kaspi) → paid|expired|cancelled|refunded (терминальные).
| Статус | Значение |
|---|---|
| pending | Создан в БД, ещё не отправлен в Kaspi |
| success | Отправлен в Kaspi, ждёт оплаты |
| paid | Оплачен (подтверждено poll'ом или вебхуком) |
| expired | Просрочен — Kaspi пометил или вышел TTL (24 часа) |
| cancelled | Отменён продавцом или Kaspi |
| refunded | Возврат после оплаты |
В модели Kaspi нет статуса «плательщик отказался»: клиент либо платит, либо счёт истекает через 24 часа (expired). Отказ плательщика в приложении Kaspi приходит как cancelled.
Статусы ошибок отправки — счёт сохранён, но до Kaspi не дошёл; детали в поле error:
| Статус | Значение |
|---|---|
| auth_expired | Сессия кассира истекла, нужен re-auth по SMS |
| banned | Kaspi заблокировал кассира (анти-фрод) |
| rate_limited | Kaspi ограничил частоту запросов |
| network_error | Сетевая ошибка при вызове Kaspi |
| invalid_response | Kaspi ответил в неожиданном формате |
| error | Неклассифицированная ошибка |
| skipped_stale | Пропущено окно отправки (> 12 часов), счёт не отправлен |
Платёжные ссылки (payment_links)
Оплата без телефона клиента. Ты создаёшь ссылку, отправляешь её сам (WhatsApp, чат, QR на экране) — клиент открывает pay.kaspi.kz и платит. Push от Kaspi НЕ отправляется, телефон клиента не нужен. Один Kaspi-токен = один платёж.
/v1/payment_linksСоздать платёжную ссылку. Idempotency-Key обязателен. Поля external_id и metadata — те же, что у счетов.
curl -X POST https://agentpay.kz/api/v1/payment_links \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"amount_kzt":5000,"comment":"Абонемент, июль","external_id":"ORD-42"}'Ответ — 201 с ресурсом payment_link. Если Kaspi отклонил выпуск токена — 422 с кодом ошибки (auth_expired, banned, …); ссылка при этом сохранена со статусом ошибки, её можно перечитать GET-ом.
/v1/payment_linksСписок ссылок, новые первыми. Фильтры status, external_id, cursor-пагинация.
/v1/payment_links/{id}Одна ссылка по UUID. Пока ссылка открыта, запрос синхронно опрашивает Kaspi (on-demand reconcile).
Пока ссылка открыта (status success), статус в ответе актуальный: переход (paid и т.д.) сохраняется и порождает событие + вебхук. В ответ добавляется поле kaspi_status_raw — сырой ответ Kaspi.
/v1/payment_links/{id}/refreshПеревыпустить Kaspi-токен за той же ссылкой. id и pay_page_url не меняются, pay_url — новый. Тело пустое.
На закрытой ссылке — 409 invalid_state.
Payment link resource
{
"id": "8e9d2b4a-1f5c-4a3d-9b8a-c7e1f0d3a5b6",
"object": "payment_link",
"status": "success",
"mode": "live",
"amount_kzt": 5000,
"comment": "Абонемент, июль",
"external_id": "ORD-42",
"metadata": null,
"pay_url": "https://pay.kaspi.kz/pay/AQyfSDPB...",
"pay_page_url": "https://agentpay.kz/pay/Py1L9FpqkTvg...",
"kaspi_operation_id": "987654321",
"receipt_url": null,
"created_at": "2026-09-07T18:27:45.752Z",
"ran_at": "2026-09-07T18:27:45.771Z",
"paid_at": null,
"expired_at": null,
"cancelled_at": null,
"refunded_at": null,
"error": null
}Статусы — те же, что у счетов. Два URL в ресурсе:
pay_url— прямая Kaspi-ссылкаpay.kaspi.kz/pay/<token>. Одноразовая: живёт до первого открытия экрана оплаты.pay_page_url— стабильная hosted-страница оплаты (/pay/<token>на нашем домене). Никогда не меняется и сама перевыпускает Kaspi-токен, когда предыдущий сгорел. Отправляй клиенту её.
Одноразовость Kaspi-ссылок
Kaspi-токен одноразовый: если клиент открыл экран оплаты и закрыл его не заплатив, тот же pay_url больше не откроется. Три решения:
- Отправлять
pay_page_url(рекомендуется) — страница сама выдаст клиенту свежий токен по кнопке. - Дёрнуть
POST /v1/payment_links/{id}/refreshи отправить новыйpay_url. - Создать новый payment_link.
Двойная оплата исключена: вытесненные токены отслеживаются сервером — первый платёж засчитывается, повторный автоматически возвращается (событие invoice.duplicate_refunded).
События
Все изменения состояния пишутся в неизменяемую таблицу events и разлетаются по подписанным webhook endpoint'ам. У каждого события уникальный UUID id.
/v1/eventsСписок событий, новые первыми, cursor-пагинация. Фильтры type, resource_id, created_after (ISO-8601).
GET /v1/events?type=invoice.paid&limit=20
GET /v1/events?resource_id=8e9d2b4a-...
GET /v1/events?type=invoice.paid&created_after=2026-09-07T00:00:00Z/v1/events/{id}Одно событие плюс история доставок вебхука по нему — ответ на вопрос «дошёл ли invoice.paid по брони X до моего сервера»:
{
"id": "6f1e8a2c-...", "object": "event", "type": "invoice.paid", "mode": "live",
"resource_type": "invoice", "resource_id": "8e9d2b4a-...",
"data": { "kaspiStatus": "Confirmed", "kaspiInvoiceId": "1234567" },
"created_at": "2026-09-07T18:35:18.000Z",
"deliveries": [
{
"id": "d1…", "object": "webhook_delivery",
"endpoint_id": "e1…", "endpoint_url": "https://your-app.com/webhooks/agentpay",
"event_id": "6f1e8a2c-...", "status": "success",
"attempt_count": 1, "next_attempt_at": null, "last_response_status": 200,
"created_at": "2026-09-07T18:35:19.000Z", "updated_at": "2026-09-07T18:35:20.000Z"
}
]
}Статусы доставки: pending (в очереди / ждёт повтора), success (получен 2xx), dead_letter (8 попыток исчерпаны или endpoint отключён).
/v1/events/{id}/resendПереотправить вебхук. Создаёт новую доставку (свежий график повторов) на все активные endpoint'ы, подписанные на тип события, или на один — если передать { "endpoint_id": "…" }. Ответ 202 со списком поставленных в очередь доставок. Idempotency-Key обязателен.
curl -X POST https://agentpay.kz/api/v1/events/6f1e8a2c-.../resend \
-H "Authorization: Bearer sk_live_..." \
-H "Idempotency-Key: $(uuidgen)"/v1/events/streamServer-Sent Events. Стрим открыт пока соединение живо (15s ping, 30min idle disconnect). Лимиты: 1 одновременное соединение на ключ, 100 глобально (иначе 429 too_many_connections_per_key / too_many_connections). Используется CLI-командами agentpay listen и agentpay events tail.
Типы событий
| Тип | Когда |
|---|---|
| invoice.created | Счёт или платёжная ссылка созданы в нашей БД |
| invoice.sent | Отправлен в Kaspi: push ушёл клиенту (счёт) или токен выпущен (ссылка) |
| invoice.paid | Клиент оплатил |
| invoice.expired | Просрочен (24 часа без оплаты) |
| invoice.cancelled | Отменён продавцом, через API или отклонён клиентом в Kaspi |
| invoice.refunded | Возврат успешно прошёл |
| invoice.refund_failed | Возврат не прошёл |
| invoice.send_failed | Не удалось отправить в Kaspi (детали в data.code) |
| invoice.duplicate_refunded | Повторная оплата по вытесненному токену автоматически возвращена (основной платёж засчитан) |
| subscription.created | Подписка создана |
| subscription.advanced | next_run_at сдвинулся (новый инвойс) |
| subscription.paused | Подписка на паузе |
| subscription.resumed | Подписка возобновлена |
| subscription.failed | Очередной счёт по подписке не выставился (ошибка Kaspi) |
| subscription.cancelled | Подписка отменена |
| cashier.activated | Кассир прошёл SMS-верификацию |
| cashier.replaced | Кассир заменён на нового |
| cashier.removed | Кассир удалён (rotation) |
| cashier.banned | Kaspi заблокировал |
| cashier.auth_expired | Сессия истекла, нужен re-auth |
«Failed» в терминах интегратора — это три события: invoice.send_failed (не удалось выставить), invoice.expired (клиент не оплатил) и invoice.refund_failed (возврат не прошёл).
При перевыпуске токена платёжной ссылки (POST /refresh или кнопка на pay-странице) приходит invoice.sent с data.refreshed: true — это не новый счёт, а свежий токен за тем же ресурсом.
Webhooks
Подключи endpoint в /app/settings/webhooks. При создании ты получишь whsec_* — секрет для проверки подписи. Показывается ОДИН раз, храни в .env или secret manager.
Формат запроса
POST /your-webhook-handler
Content-Type: application/json
X-AgentPay-Signature: t=1735689600,v1=5257a86<...>
X-AgentPay-Event-Type: invoice.paid
X-AgentPay-Event-Id: 6f1e8a2c-9d3b-4c7e-8a1f-2b5d9c0e4a7b
User-Agent: AgentPay-Webhooks/1.0Тело — Event resource
Для событий invoice.* поле data дополняется актуальными полями счёта, чтобы не делать GET за каждой оплатой:
{
"id": "6f1e8a2c-9d3b-4c7e-8a1f-2b5d9c0e4a7b",
"object": "event",
"type": "invoice.paid",
"account_id": "3c1f…",
"resource_type": "invoice",
"resource_id": "8e9d2b4a-1f5c-4a3d-9b8a-c7e1f0d3a5b6",
"mode": "live",
"data": {
"kaspiStatus": "Confirmed",
"kaspiInvoiceId": "1234567",
"status": "paid",
"kind": "one_time",
"amount_kzt": 1500,
"client_phone": "77001234567",
"paid_at": "2026-09-07T18:35:18.000Z",
"external_id": "BK-2026-000123",
"metadata": { "room": "12A", "guests": 2 },
"kaspi_invoice_id": "1234567",
"receipt_url": "https://kaspi.kz/receipt/1234567"
},
"created_at": "2026-09-07T18:35:18.000Z"
}id события — уникальный UUID; храни его и игнорируй повторы (доставка at-least-once). resource_id — id счёта, data.external_id — твой booking_id / order_id.
Проверка подписи
HMAC-SHA256 от строки <timestamp>.<raw_body> с твоим whsec_* в качестве ключа. Сравнивай constant-time, отвергай запросы старше 5 минут. Подписывается сырое тело — не пересериализуй JSON перед проверкой.
import crypto from 'node:crypto';
function verifyAgentPayWebhook(rawBody: string, header: string, secret: string): boolean {
const parts = Object.fromEntries(
header.split(',').map((p) => p.trim().split('=', 2) as [string, string]),
);
const t = Number(parts.t);
const sig = parts.v1;
if (!t || !sig) return false;
if (Math.abs(Date.now() / 1000 - t) > 300) return false; // 5min replay window
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(sig, 'hex'));
}То же на Python:
import hmac, hashlib, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t, sig = int(parts["t"]), parts["v1"]
if abs(time.time() - t) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)Доставка
- Timeout: 15 секунд. Дольше — считаем неудачей.
- Успех = HTTP 2xx. Любой не-2xx — повтор по графику ниже.
- Повторные попытки: 1 минута → 5 минут → 30 минут → 2 часа → 12 часов → 1 день → 2 дня → dead-letter (всего 8 попыток, ~3 дня).
- Идемпотентный приёмник обязателен — мы гарантируем at-least-once. Дедуплицируй по
idсобытия.
Потерянные события: переотправка и догон
Три инструмента, от простого к надёжному:
- Переотправить вручную — кнопка «Переотправить» в /app/settings/webhooks или
POST /v1/events/{id}/resend(agentpay events resend <id>). Историю доставок по событию смотри вGET /v1/events/{id}. - Догнать после простоя — после того как твой сервер полежал дольше 3 дней (или ты хочешь сверку по расписанию), опроси
GET /v1/events?type=invoice.paid&created_after=<последняя обработанная метка>и пройди по курсору. Это тот же журнал, из которого шлются вебхуки. - Сверка по счетам —
GET /v1/invoices?status=paid&paid_after=…возвращает полные ресурсы (сумма,external_id,kaspi_invoice_id, чек). Удобно для бухгалтерии и 1С.
Обмен с 1С
Готового модуля для 1С нет — обмен строится на REST API. Два варианта, поля одинаковые.
Вариант 1 — 1С забирает оплаты сама (pull, рекомендуем для старта)
Регламентное задание раз в 5–15 минут вызывает GET /v1/invoices?status=paid&paid_after=<метка>, создаёт документы и запоминает максимальный paid_at из ответа как новую метку. Дедупликация — по id счёта (сохраняй его в реквизит документа). Если счётов больше limit, иди по next_cursor.
// 1С:Предприятие 8.3, BSL. КлючAPI — sk_live_* из /app/settings/api-keys.
Соединение = Новый HTTPСоединение("agentpay.kz", 443, , , , 30, Новый ЗащищенноеСоединениеOpenSSL());
Заголовки = Новый Соответствие;
Заголовки.Вставить("Authorization", "Bearer " + КлючAPI);
Заголовки.Вставить("Accept", "application/json");
Путь = "/api/v1/invoices?status=paid&limit=100&paid_after=" + КодироватьСтроку(МеткаISO, СпособКодированияСтроки.КодировкаURL);
Ответ = Соединение.Получить(Новый HTTPЗапрос(Путь, Заголовки));
Если Ответ.КодСостояния <> 200 Тогда
ВызватьИсключение "AgentPay: HTTP " + Ответ.КодСостояния + " " + Ответ.ПолучитьТелоКакСтроку();
КонецЕсли;
Чтение = Новый ЧтениеJSON;
Чтение.УстановитьСтроку(Ответ.ПолучитьТелоКакСтроку());
Результат = ПрочитатьJSON(Чтение, Ложь); // Структура: object, data (Массив), has_more, next_cursor
Для Каждого Счет Из Результат.data Цикл
// Счет.id, Счет.external_id, Счет.amount_kzt, Счет.paid_at, Счет.client_phone,
// Счет.kaspi_invoice_id, Счет.receipt_url, Счет.comment, Счет.metadata
СоздатьОплатуОтПокупателя(Счет);
КонецЦикла;Вариант 2 — вебхук в вашу прослойку (push)
AgentPay отправляет invoice.paid / invoice.refunded на ваш HTTP-сервис (в том числе опубликованный HTTP-сервис 1С). Проверьте подпись X-AgentPay-Signature, ответьте 2xx, дедуплицируйте по id события. Всё нужное для документа уже лежит в data (см. «Тело — Event resource»). Если ваш сервер был недоступен, события догоняются вариантом 1.
Маппинг полей
| Поле AgentPay | Куда в 1С | Комментарий |
|---|---|---|
| id | Реквизит «ID платежа AgentPay» | Ключ дедупликации, UUID |
| external_id | Документ-основание (Заказ / Бронь) | Ваш номер, передаётся при создании счёта |
| amount_kzt | Сумма документа | Целое число тенге |
| paid_at | Дата документа | ISO-8601, UTC |
| client_phone | Контрагент (поиск по телефону) | 11 цифр |
| kaspi_invoice_id | Номер операции Kaspi | Для сверки с выпиской Kaspi |
| receipt_url | Комментарий / ссылка на чек | Чек Kaspi |
| comment | Назначение платежа | Виден плательщику |
| metadata | Любые доп. реквизиты | Объект key→value |
| status = "refunded", refunded_at | Возврат покупателю | Отдельный документ |
Сумма приходит на расчётный счёт мерчанта от Kaspi по обычному регламенту Kaspi Pay; AgentPay фиксирует факт оплаты и её реквизиты, не является платёжным агентом и не держит деньги.
CLI
@kaspi-agentpay/cli — обёртка над REST API, та же функциональность за одну команду.
npm install -g @kaspi-agentpay/cli
agentpay init --base-url https://agentpay.kz/api
# вставь sk_test_… (песочница) или sk_live_… когда спросит
# создать счёт с твоими идентификаторами
agentpay invoices create --phone 77001234567 --amount 1500 --comment "Бронь #123" \
--external-id BK-2026-000123 --metadata '{"room":"12A"}'
# список + фильтры
agentpay invoices list --status paid --paid-after 2026-09-01T00:00:00Z
agentpay invoices get <invoice_id>
# отменить / вернуть
agentpay invoices cancel <invoice_id>
agentpay invoices refund <invoice_id>
# платёжные ссылки без телефона
agentpay links create --amount 5000 --external-id ORD-42
agentpay links refresh <payment_link_id>
# песочница: симулировать оплату / просрочку (sk_test_* only)
agentpay test pay <invoice_id>
agentpay test expire <invoice_id>
# события и вебхуки
agentpay events tail # сырой лог (SSE)
agentpay events get <event_id> # событие + история доставок
agentpay events resend <event_id> # переотправить вебхук
agentpay listen --forward-to http://localhost:3000/webhooks/agentpayCLI хранит ключ в ~/.agentpay/config.json (mode 0600). Переопределяй через env AGENTPAY_API_KEY и AGENTPAY_BASE_URL.
Для AI-агентов (Claude Code, Codex, Cursor)
AgentPay спроектирован под агентов: одна страница документации (вот эта), OpenAPI-спека, один CLI с предсказуемыми флагами, плоский набор REST-эндпоинтов с типизированными ошибками.
Скармливай агенту всю спецификацию одной командой
Жми «Скопировать всю документацию» в шапке этой страницы — markdown сразу в буфер. Или подтяни через curl:
curl -s https://agentpay.kz/api/docs.md | pbcopy # → в буфер для вставки в чат агента
curl -s https://agentpay.kz/api/openapi.json > openapi.json # → для кодогенерации клиентаИли через флаг --system / context-инъекцию агента:
claude code --system "$(curl -s https://agentpay.kz/api/docs.md)" \
"Выстави клиенту 77001234567 счёт на 5000 ₸ за Pro-подписку, используя AgentPay CLI."Типичные промпты, которые агент сможет выполнить после инжеста
| Запрос пользователя | Что сделает агент |
|---|---|
| «Выстави Алие счёт на 12 000 за консультацию» | agentpay invoices create \ --phone 7705... \ --amount 12000 \ --comment "Консультация" |
| «Покажи, что не оплачено за неделю» | agentpay invoices list \ --status success \ --limit 50 # + фильтрация по дате на клиенте |
| «Сделай возврат по последнему счёту Алии» | agentpay invoices list # → найти id agentpay invoices refund <id> |
| «Подключи мой бэкенд к вебхукам и проверь что invoice.paid доходит» | agentpay listen \ --forward-to http://localhost:3000/wh # + создать тестовый счёт agentpay test pay <id> |
Правила, которые агент должен помнить
- Каждый
POSTсоздающий ресурс требуетIdempotency-Key— генерируйuuidgenна каждой логической операции, не на каждый retry. - Ошибки всегда JSON
{ "error": { "type", "message" } }—typeстабильное,message— для человека. - Status lifecycle инвойса:
pending→success(отправлен) →paid|expired|cancelled|refunded(терминальные).refundвозможен только наpaid,cancel— наpending|success. - Для оплаты без телефона клиента используй
POST /v1/payment_linksи отправляй клиентуpay_page_url(стабильная страница), а не одноразовыйpay_url. - Для разработки используй
sk_test_*— это песочница: Kaspi не вызывается, деньги не списываются, статусы переключаются черезPOST /v1/test/invoices/{id}/pay|expire. - Передавай свой
external_idпри создании — по нему сверяй вебхуки и списки.
Формат ошибок
Все ошибки — JSON, одинаковая форма:
{
"error": {
"type": "no_active_cashier",
"message": "No active cashier on this tenant. Add one in the web app first."
}
}| HTTP | type | Когда |
|---|---|---|
| 400 | invalid_request | zod-валидация не прошла (в message — какое поле) |
| 400 | invalid_json | тело не JSON |
| 400 | idempotency_key_required | нет Idempotency-Key на мутирующем POST |
| 401 | authentication_required | нет Authorization header |
| 401 | invalid_api_key | ключ не найден или отозван |
| 402 | subscription_required | подписка тенанта не активна (trial или past_due) |
| 403 | forbidden_scope | pk_ ключ на secret endpoint |
| 403 | api_requires_pro | API и CLI доступны только на тарифе Pro |
| 403 | test_mode_required | sandbox-эндпоинт вызван боевым ключом |
| 404 | not_found | ресурс не существует, принадлежит другому тенанту или другому режиму (live/test) |
| 404 | endpoint_not_found | resend на несуществующий/отключённый webhook endpoint |
| 409 | no_active_cashier | у тенанта нет активного кассира (боевой режим) |
| 409 | cashier_not_ready | кассир есть, но status != active |
| 409 | wrong_kind | cancel применим только к разовым счетам |
| 409 | not_sent | счёт не был принят Kaspi — отменять нечего |
| 409 | already_paid | отмена счёта, который уже оплачен — делай возврат |
| 409 | already_cancelled | счёт уже отменён |
| 409 | not_paid | возврат на неоплаченном счёте — refund только для paid |
| 409 | no_cashier | у счёта/ссылки нет привязанного кассира |
| 409 | invalid_state | операция невозможна в текущем статусе (напр. refresh закрытой ссылки, test pay на отменённом) |
| 409 | idempotency_conflict | тот же ключ + другое тело |
| 409 | no_webhook_endpoints | resend, но ни один активный endpoint не подписан на этот тип события |
| 422 | auth_expired / banned / rate_limited / network_error / invalid_response / error | Kaspi отклонил выпуск при создании — счёт/ссылка сохранены со статусом ошибки, перечитай GET-ом |
| 429 | rate_limited | превышен лимит запросов в минуту |
| 429 | too_many_connections / too_many_connections_per_key | SSE: превышен лимит соединений (100 глобально / 1 на ключ) |
| 500 | internal_error | неожиданная ошибка сервера |
| 502 | kaspi_error | Kaspi отклонил вызов |
Пагинация
Списковые эндпоинты возвращают объект:
{
"object": "list",
"data": [ ... ],
"has_more": true,
"next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wNS0xMiIsImlkIjoiOGU5ZDJiNGEifQ"
}Передавай next_cursor в параметре cursor следующего запроса. limit по умолчанию 20, максимум 100. Порядок — новые первыми.