# AgentPay API

REST API для приёма платежей через Kaspi. Аутентификация по ключу, идемпотентность, подписанные вебхуки, песочница, CLI для AI-агентов и людей.

Base URL: `https://agentpay.kz/api`
OpenAPI 3.1: `https://agentpay.kz/api/openapi.json`
Эта страница в markdown: `https://agentpay.kz/api/docs.md`

---

## С чего начать

AgentPay даёт три способа принимать оплату через Kaspi: веб-интерфейс, REST API и CLI. Веб работает сразу после регистрации. Для API и CLI:

1. Создай ключ в `/app/settings/api-keys`. Для разработки — `sk_test_*` (песочница, деньги не списываются, кассир Kaspi не нужен). Для боя — `sk_live_*` и подключённый кассир в `/app/cashier`.
2. Сделай первый запрос:

```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` вернутся в ресурсе и в каждом вебхуке по этому счёту.

3. Подключи 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 не нужен.
- Оплату, просрочку и возврат ты вызываешь сам — при этом уходят **те же события и подписанные вебхуки**, что и в бою:

```bash
# счёт оплачен → 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 глобально**.

Каждый ответ несёт заголовки:

```http
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 53
X-RateLimit-Reset: 1735689600
```

На превышении — `429 Too Many Requests` с `Retry-After` в секундах.

---

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

POST-эндпоинты, создающие или мутирующие ресурсы, требуют заголовок `Idempotency-Key`. Сгенерируй UUID на стороне клиента — повторный запрос с тем же ключом и тем же телом вернёт закешированный ответ (24 часа).

```bash
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`) — тогда ретрай из любой части системы гарантированно не создаст второй счёт.

---

## Счета

### POST /v1/invoices
Создать разовый счёт: Kaspi шлёт push на телефон клиента, плюс ты получаешь `pay_link_url` для отправки любым каналом. `Idempotency-Key` обязателен.

Тело:
```json
{
  "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`.

### GET /v1/invoices
Список счетов, новые первыми. Cursor-пагинация, фильтры:

```http
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С»).

### GET /v1/invoices/{id}
Один счёт по UUID. 404 если не найден, принадлежит другому тенанту или другому режиму.

### POST /v1/invoices/{id}/cancel
Отменить неоплаченный счёт. Тело пустое. `Idempotency-Key` обязателен.

### POST /v1/invoices/{id}/refunds
Возврат оплаченного счёта. Возвращается полная сумма. `Idempotency-Key` обязателен.

### Invoice resource

```json
{
  "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-токен = один платёж.

### POST /v1/payment_links
Создать платёжную ссылку. `Idempotency-Key` обязателен. Поля `external_id` и `metadata` — те же, что у счетов.

```bash
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-ом.

### GET /v1/payment_links
Список ссылок, новые первыми. Фильтры `status`, `external_id`, cursor-пагинация.

### GET /v1/payment_links/{id}
Одна ссылка по UUID. Пока ссылка открыта (status `success`), запрос синхронно опрашивает Kaspi (on-demand reconcile): статус в ответе актуальный, переход (`paid` и т.д.) сохраняется и порождает событие + вебхук. В ответ добавляется поле `kaspi_status_raw` — сырой ответ Kaspi.

### POST /v1/payment_links/{id}/refresh
Перевыпустить Kaspi-токен за той же ссылкой. `id` и `pay_page_url` не меняются, `pay_url` — новый. Тело пустое. На закрытой ссылке — `409 invalid_state`.

### Payment link resource

```json
{
  "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` больше не откроется. Три решения:

1. **Отправлять `pay_page_url`** (рекомендуется) — страница сама выдаст клиенту свежий токен по кнопке.
2. Дёрнуть `POST /v1/payment_links/{id}/refresh` и отправить новый `pay_url`.
3. Создать новый payment_link.

Двойная оплата исключена: вытесненные токены отслеживаются сервером — первый платёж засчитывается, повторный автоматически возвращается (событие `invoice.duplicate_refunded`).

---

## События

Все изменения состояния пишутся в неизменяемую таблицу `events` и разлетаются по подписанным webhook endpoint'ам. У каждого события уникальный UUID `id`.

### GET /v1/events
Список событий, новые первыми, cursor-пагинация. Фильтры `type`, `resource_id`, `created_after` (ISO-8601).

```http
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
```

### GET /v1/events/{id}
Одно событие плюс история доставок вебхука по нему — ответ на вопрос «дошёл ли `invoice.paid` по брони X до моего сервера»:

```json
{
  "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 отключён).

### POST /v1/events/{id}/resend
Переотправить вебхук. Создаёт новую доставку (свежий график повторов) на все активные endpoint'ы, подписанные на тип события, или на один — если передать `{ "endpoint_id": "…" }`. Ответ `202` со списком поставленных в очередь доставок. `Idempotency-Key` обязателен.

```bash
curl -X POST https://agentpay.kz/api/v1/events/6f1e8a2c-.../resend \
  -H "Authorization: Bearer sk_live_..." \
  -H "Idempotency-Key: $(uuidgen)"
```

### GET /v1/events/stream
Server-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.

### Формат запроса

```http
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 за каждой оплатой:

```json
{
  "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 перед проверкой.

```typescript
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:

```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` события.

### Потерянные события: переотправка и догон

Три инструмента, от простого к надёжному:

1. **Переотправить вручную** — кнопка «Переотправить» в `/app/settings/webhooks` или `POST /v1/events/{id}/resend` (`agentpay events resend <id>`). Историю доставок по событию смотри в `GET /v1/events/{id}`.
2. **Догнать после простоя** — после того как твой сервер полежал дольше 3 дней (или ты хочешь сверку по расписанию), опроси `GET /v1/events?type=invoice.paid&created_after=<последняя обработанная метка>` и пройди по курсору. Это тот же журнал, из которого шлются вебхуки.
3. **Сверка по счетам** — `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`.

```bsl
// 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, та же функциональность за одну команду.

```bash
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/agentpay
```

CLI хранит ключ в `~/.agentpay/config.json` (mode 0600). Переопределяй через env `AGENTPAY_API_KEY` и `AGENTPAY_BASE_URL`.

---

## Для AI-агентов (Claude Code, Codex, Cursor)

AgentPay спроектирован под агентов: одна страница документации (вот эта), OpenAPI-спека, один CLI с предсказуемыми флагами, плоский набор REST-эндпоинтов с типизированными ошибками.

**Скармливай агенту всю спецификацию одной командой:**

```bash
curl -s https://agentpay.kz/api/docs.md | pbcopy        # → в буфер для вставки в чат агента
curl -s https://agentpay.kz/api/openapi.json > openapi.json   # → для кодогенерации клиента
```

Или через флаг `--system` / context-инъекцию агента:

```bash
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, одинаковая форма:

```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 отклонил вызов |

---

## Пагинация

Списковые эндпоинты возвращают объект:

```json
{
  "object": "list",
  "data": [ ... ],
  "has_more": true,
  "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wNS0xMiIsImlkIjoiOGU5ZDJiNGEifQ"
}
```

Передавай `next_cursor` в параметре `cursor` следующего запроса. `limit` по умолчанию 20, максимум 100. Порядок — новые первыми.
