# Справочник Aitole API

Базовый адрес: `https://api.aitole.kz`

Приём Kaspi Pay по REST поверх аккаунта мерчанта. Все суммы — целые тенге.
Время — ISO 8601 в UTC. Тела запросов и ответов — JSON.

## Два контура доступа

| контур | заголовок | что делает |
|---|---|---|
| ключ интеграции | `X-API-Key: sk_live_…` / `sk_test_…` | принимает платежи: `/v2/*` |
| токен кабинета | `Authorization: Bearer …` | управляет аккаунтом: `/me/*` |

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

**Режим ключа жёстко связан с контуром подключения.** `sk_test_` работает только
с песочницей, `sk_live_` только с настоящим Kaspi. Попытка смешать даёт
`403 key_mode_mismatch`. Поэтому тестовым ключом невозможно списать настоящие деньги.

## Ошибка всегда выглядит одинаково

```json
{
  "error": {
    "code": "validation_error",
    "message": "Сумма должна быть целым числом тенге",
    "category": "permanent",
    "request_id": "req_0f3c…"
  }
}
```

`category` говорит, что делать: `transient` — повторить, `permanent` — не повторять,
`auth` — чинить доступ, `rate_limit` — подождать `Retry-After`.

## Выставить QR

```bash
curl -X POST https://api.aitole.kz/v2/qr \
  -H "X-API-Key: sk_test_…" \
  -H "Idempotency-Key: order-1024" \
  -H "Content-Type: application/json" \
  -d '{"amount": 5000, "comment": "Заказ №1024"}'
```

Ответ — операция со статусом `pending`, полем `qr_token` (ссылка для покупателя)
и `expires_at`. Покажите ссылку клиенту; оплату подтвердит событие, а не этот ответ.

**Провайдер может отказать с HTTP 200.** Тогда в ответе придёт `status: "failed"`
и `failure_reason` с `failure_message` от Kaspi. Это не ошибка транспорта, проверяйте статус.

## Счёт на номер телефона

```bash
curl -X POST https://api.aitole.kz/v2/invoices \
  -H "X-API-Key: sk_live_…" \
  -H "Idempotency-Key: order-1025" \
  -H "Content-Type: application/json" \
  -d '{"amount": 12000, "phone": "+77001234567", "comment": "Заказ №1025"}'
```

Покупателю приходит счёт в приложение Kaspi. Номер — в формате E.164;
к виду, который ждёт Kaspi, платформа приводит сама.

## Регулярные счета

Автосписаний у Kaspi нет — поэтому регулярный платёж здесь это счёт, который сам
уходит клиенту по расписанию, а клиент оплачивает его в Kaspi в одно касание.

```bash
curl -X POST https://api.aitole.kz/v2/subscriptions \
  -H "X-API-Key: sk_live_…" \
  -H "Idempotency-Key: sub-client-77" \
  -H "Content-Type: application/json" \
  -d '{"phone": "+77001234567", "amount": 4990, "period": "month", "billing_day": 5, "description": "Курс, {today}"}'
```

- `period`: `week`, `month` или `quarter`; `billing_day` (неделя 1–7, месяц 1–28)
  или `billing_day_from_end` (0 — последний день месяца); `billing_time` — «ЧЧ:ММ»
  по Алматы, 06:00–22:00, по умолчанию 13:00.
- Первый счёт: `bill_immediately: true` — сразу, `first_billing_at` — с даты,
  иначе через период. `total_cycles` — сколько платежей всего.
- Не оплатил: повтор `max_retry_attempts` раз (по умолчанию 3) через
  `retry_interval_hours` (24). Дальше `on_unpaid: "expire"` — льготный период
  `grace_period_days` (3) и окончание, `"continue"` — следующий период. Открытым
  всегда остаётся один счёт.
- Клиент отклонил счёт в Kaspi — регулярный счёт отменяется сразу
  (`subscription.cancelled`, `reason: "payer_refused"`).
- Касса не принимает платежи — счёт ждёт переподключения, попытка не сгорает.
- Номер хранится зашифрованным и возвращается только маской. Номер не клиента
  Kaspi отвергается сразу: `422 payer_not_kaspi_client`.
- Счета подписки — обычные операции с полями `subscription_id`,
  `subscription_cycle`, `subscription_attempt`. События `subscription.*` — в
  журнале и вебхуках.

## Постоянные ссылки на оплату

QR Kaspi живёт 5 минут, поэтому постоянная ссылка — это страница
`aitole.kz/pay/<адрес>`, которая по нажатию «Оплатить» создаёт свежий QR: на
телефоне сразу открывается Kaspi, на компьютере показывается QR.

```bash
curl -X POST https://api.aitole.kz/v2/payment-links \
  -H "X-API-Key: sk_live_…" -H "Content-Type: application/json" \
  -d '{"title": "Консультация", "amount": 4990, "merchant_name": "Психолог Айжан"}'
```

- `amount` — фиксированная сумма; без неё клиент вводит сумму сам в пределах
  `min_amount`/`max_amount`.
- `ask_name` (по умолчанию `true`) — спросить имя клиента: по QR Kaspi не
  сообщает, кто заплатил. Имя попадёт в `payer_name` операции.
- `slug` — свой адрес; занят — `409 slug_taken`. Без него адрес строится из названия.
- Открытие страницы ничего не создаёт: превью ссылки в мессенджерах не тратят
  лимит неоплаченных. QR создаётся только нажатием.
- Оплата — обычная операция и `payment.paid` с полем `payment_link_id`.
  `GET /v2/payment-links` отдаёт `paid_count` и `paid_total`, `PATCH` меняет и
  выключает (`"status": "disabled"`).

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

`Idempotency-Key` обязателен для всех операций, создающих платёж. Повтор с тем же
ключом возвращает тот же результат, а не второй счёт. Ключ уникален **в пределах
аккаунта**, а не проекта: если два ваших проекта нумеруют заказы независимо,
добавьте префикс — `shop-a-1024`, а не `1024`.

## Узнать об оплате

Два способа, и лучше оба.

**Вебхук** — приходит сам за несколько секунд.

```
POST ваш-адрес
X-Webhook-Signature: sha256=<hex>
Content-Type: application/json

{"id":"evt_…","type":"payment.paid","created_at":"…","data":{…}}
```

Подпись — HMAC-SHA256 от строки `"{timestamp}.{сырое тело}"` секретом эндпоинта.
Проверяйте её по сырому телу до `JSON.parse`. Свежесть — 300 секунд.
Неудачные доставки повторяются через 1 мин, 5 мин, 30 мин, 2 ч, 6 ч и 24 ч;
после двух суток непрерывных отказов эндпоинт отключается.

**Журнал событий** — `GET /v2/events`, курсорная пагинация. Резерв на случай,
когда вебхук не дошёл: история остаётся здесь всегда.

Типы событий: `payment.created`, `payment.paid`, `payment.expired`,
`payment.cancelled`, `payment.refunded`, `connection.session_expired`,
`connection.restored`, `billing.past_due`, `billing.downgraded`.

Каждое платёжное событие несёт `connection_id` — через какую кассу прошёл платёж.

## Возврат

```bash
curl -X POST https://api.aitole.kz/v2/refunds \
  -H "X-API-Key: sk_live_…" \
  -H "Idempotency-Key: refund-1024-1" \
  -H "Content-Type: application/json" \
  -d '{"operation_id": "…", "amount": 5000}'
```

Полный или частичный. Операция остаётся `paid`: возврат ложится поверх платежа,
а не отменяет его. Окно — 90 дней. Нужен ключ со scope `refund`.

## Песочница

Песочница — это подключение с драйвером `mock`. Настоящий Kaspi не участвует,
деньги не двигаются, `qr_token` заведомо ненастоящий.

```bash
# довести песочный счёт до оплаты
curl -X POST https://api.aitole.kz/v2/sandbox/operations/{id}/pay -H "X-API-Key: sk_test_…"
# или просрочить
curl -X POST https://api.aitole.kz/v2/sandbox/operations/{id}/expire -H "X-API-Key: sk_test_…"
# или «клиент отклонил счёт»
curl -X POST https://api.aitole.kz/v2/sandbox/operations/{id}/reject -H "X-API-Key: sk_test_…"
# регулярный счёт: следующий шаг сейчас, не дожидаясь даты
curl -X POST https://api.aitole.kz/v2/sandbox/subscriptions/{id}/bill-now -H "X-API-Key: sk_test_…"
```

Эти ручки меняют состояние **у провайдера**, а статус вашей операции двигает тот
же поллер, что и в бою, с той же задержкой. Вы проверяете ровно тот код, который
отработает на живых деньгах.

## Несколько Kaspi-касс

У аккаунта может быть несколько боевых подключений. Касса выбирается по порядку:
`connection_id` в запросе → касса, к которой привязан ключ → основная касса →
единственная активная.

Привязка ключа — **граница**, а не умолчание: ключ кассы A не выставит счёт
на кассу B, даже если передать её `connection_id`, ответ будет `403 forbidden`.
Если активных касс несколько и выбор не сделан — `409 connection_ambiguous`.

## Сессия Kaspi

У Kaspi одна активная сессия на номер. Вход в приложение Kaspi Pay под номером
кассира вытесняет сессию платформы: подключение уходит в `session_expired`,
новые платежи отвергаются с `409 no_active_connection`, в журнал пишется
`connection.session_expired`. Восстановление — повторный вход по SMS в кабинете.

Поэтому номер кассира, отданный платформе, должен быть выделенным.

## Лимиты

Частота запросов ограничена по ключу. Тарифные лимиты считаются по **оплаченным**
платежам, а не по выставленным счетам: брошенные покупателем счета квоту не расходуют.

## Полный список эндпоинтов

```
POST   /v2/qr                                   создать QR
POST   /v2/invoices                             счёт на телефон
GET    /v2/operations                           список операций
GET    /v2/operations/{id}                      операция по id
POST   /v2/operations/{id}/cancel               отменить
POST   /v2/refunds                              возврат
POST   /v2/subscriptions                        регулярный счёт
GET    /v2/subscriptions                        список регулярных счетов
GET    /v2/subscriptions/{id}                   регулярный счёт по id
PATCH  /v2/subscriptions/{id}                   изменить со следующего счёта
POST   /v2/subscriptions/{id}/pause             пауза
POST   /v2/subscriptions/{id}/resume            возобновить
POST   /v2/subscriptions/{id}/cancel            отменить
GET    /v2/subscriptions/{id}/invoices          счета регулярного счёта
POST   /v2/payment-links                        постоянная ссылка на оплату
GET    /v2/payment-links                        ссылки со статистикой
GET    /v2/payment-links/{id}                   ссылка по id
PATCH  /v2/payment-links/{id}                   изменить, выключить
GET    /v2/events                               журнал событий
GET    /v2/status                               health, без ключа
POST   /v2/sandbox/operations/{id}/pay          песочница: оплатить
POST   /v2/sandbox/operations/{id}/expire       песочница: просрочить
POST   /v2/sandbox/operations/{id}/reject       песочница: клиент отклонил счёт
POST   /v2/sandbox/subscriptions/{id}/bill-now  песочница: следующий шаг подписки сейчас
POST   /v2/sandbox/connections/{id}/kill-session песочница: убить сессию
```

Управление аккаунтом (`/me/*`, по токену кабинета): ключи, подключения Kaspi,
вебхуки, уведомления, подписка, операции и события на чтение.
