# API подписок — счета по расписанию

> Подписка выставляет счета по расписанию, а каждую оплату подтверждает сам покупатель. Все поля создания, интервалы, лестница повторов, политика пропуска, методы pause/resume/run и поля состояния.

## Коротко

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

Минимальный пример:

```http
POST /api/v1/subscriptions
X-API-Key: qp_live_…
Content-Type: application/json

{
  "amount": 9900,
  "interval": "month",
  "every": 1,
  "kind": "phone",
  "customer": { "phone": "77011234567", "name": "Айдана" }
}
```

Ключу нужно право **`subscriptions:manage`**.

## Что автоматизируется, а что нет

| Автоматически | Вручную / покупателем |
|---|---|
| Выпуск счёта по расписанию | Оплату подтверждает покупатель |
| Повтор неудачной попытки | Смена суммы — через изменение подписки |
| Обработка пропущенного запуска | Решение остановить подписку |
| Отправка событий (вебхуки) | Возврат денег |

То есть подписка — это расписание, которое помнит клиента и вовремя выставляет счёт, а не механизм списания средств.

## Создание: все поля

| Поле | Тип | Описание |
|---|---|---|
| `amount` | number, обязательно | Сумма одного периода, тенге |
| `interval` | `day` \| `week` \| `month` | Единица интервала |
| `every` | integer | Кратность: `interval: month`, `every: 3` — раз в квартал |
| `kind` | `qr` \| `phone` | Тип счёта. `phone` — в приложение Kaspi покупателя |
| `customer.phone` / `.name` / `.email` | string | При `kind: phone` телефон обязателен |
| `dayOfMonth` | integer | В какой день месяца выставлять счёт |
| `startAt` | date-time | Когда первый запуск |
| `maxRuns` | integer | Максимум запусков. По достижении подписка закрывается |
| `retryDelaysMin` | number[] | Лестница повторов в минутах. По умолчанию `[15, 60, 360]`, не больше 5 значений |
| `misfirePolicy` | `run_once` \| `skip` | Что делать с пропущенным запуском. По умолчанию `run_once` |
| `misfireAfterMin` | integer | Через сколько минут запуск считается пропущенным. По умолчанию 1440 |
| `description` | string | Покупатель видит в счёте |
| `metadata` | object | Любой JSON, переносится в счёт |

Каждый созданный счёт несёт поля `metadata.subscriptionId` и `metadata.run` — по ним в отчёте видно, какой подписке и какому периоду принадлежит платёж.

## Лестница повторов

Выпуск счёта может не удаться: Kaspi не ответил, оборвалась привязка кассира, закончился лимит тарифа. Тогда подписка повторяет попытку по лестнице `retryDelaysMin`.

Значение по умолчанию `[15, 60, 360]` — через 15 минут, затем через 1 час, затем через 6 часов.

| Значение | Что происходит |
|---|---|
| `[15, 60, 360]` | Три повтора: 15 мин, 1 ч, 6 ч |
| `[5, 5, 5, 5, 5]` | Пять повторов каждые 5 минут (5 значений — максимум) |
| `[]` | Повторов нет, запуск отбрасывается после первой неудачи |

Когда лестница закончилась, этот запуск **отбрасывается**, но подписка не закрывается — расписание продолжается со следующего периода. О неудаче приходит уведомление в Telegram или на email.

## Политика пропуска

Запуск может не выполниться вовремя: подписка стояла на паузе, был долгий перерыв. Что делать в этом случае, решает `misfirePolicy`:

| Значение | Поведение |
|---|---|
| `run_once` (по умолчанию) | Опоздавший запуск выполняется один раз, несколько пропущенных схлопываются в один |
| `skip` | Если опоздание больше `misfireAfterMin` (по умолчанию 1440 минут = сутки), запуск пропускается, расписание идёт дальше |

Хотите всё-таки получить оплату за пропущенный период — оставляйте `run_once`. Считаете, что устаревший счёт только запутает покупателя — ставьте `skip`.

## Методы управления

| Метод | Что делает |
|---|---|
| `POST /api/v1/subscriptions` | Создаёт подписку |
| `GET /api/v1/subscriptions` | Список |
| `GET /api/v1/subscriptions/{id}` | Одна подписка с полным состоянием |
| `PATCH /api/v1/subscriptions/{id}` | Меняет сумму, интервал, политику и другие поля |
| `POST /api/v1/subscriptions/{id}/pause` | Ставит расписание на паузу |
| `POST /api/v1/subscriptions/{id}/resume` | Возобновляет |
| `POST /api/v1/subscriptions/{id}/cancel` | Закрывает окончательно |
| `POST /api/v1/subscriptions/{id}/run` | Выставляет внеочередной счёт |

У `resume` есть важный нюанс:

```json
POST /api/v1/subscriptions/sub_9Qp1/resume
{ "catchUp": true }
```

С `catchUp: true` пропущенный за время паузы запуск выполняется **сразу**. По умолчанию (без `catchUp` или с `false`) расписание просто продолжается со следующей плановой даты.

## Поля состояния

В ответе `GET /api/v1/subscriptions/{id}` видно текущее положение дел:

| Поле | Значение |
|---|---|
| `nextRunAt` | Когда выйдет следующий счёт |
| `runs` | Сколько запусков выполнено |
| `failedRuns` | Сколько запусков завершились неудачей |
| `retryAttempt` | Какая по счёту попытка идёт по текущему запуску |
| `lastRunStatus` | `ok` \| `retrying` \| `failed` \| `skipped` |
| `lastError` | Код последней ошибки |
| `lastInvoiceId` | Последний созданный счёт |

Если по подписке не выставляются счета, диагностику начинайте именно отсюда: `lastRunStatus` и `lastError` сразу называют причину. Подробный разбор: [Подписка не выставляет счета](/kb/ru/subscription-not-running).

## Кассир

Подписка **запоминает, через какого кассира она создана**, и дальше выставляет счета через него же. Отсюда два следствия:

- Если привязка этого кассира оборвалась, счета подписки падают с `kaspi_session_expired` и уходят в лестницу повторов. После восстановления привязки расписание продолжается само.
- Если вы сменили кассира, старые подписки придётся пересоздать — они остаются привязанными к прежнему подключению.

## Коды ошибок

| Код | HTTP | Что случилось |
|---|---|---|
| `invalid_interval` | 422 | `interval` не `day`, `week` или `month` |
| `invalid_every` | 422 | `every` не положительное целое |
| `invalid_retry` | 422 | `retryDelaysMin` неверный: больше 5 значений или отрицательные числа |
| `invalid_misfire` | 422 | `misfirePolicy` не `run_once` и не `skip` |
| `invalid_max_runs` | 422 | Неверный `maxRuns` |
| `subscription_closed` | 409 | Подписка завершена или остановлена |
| `subscription_not_found` | 404 | Идентификатор не найден |
| `insufficient_scope` | 403 | У ключа нет права `subscriptions:manage` |

## События

`subscription.created` — подписка создана, `subscription.status` — изменилось состояние. Кроме того, по каждому выставленному счёту приходят обычные события `invoice.created`, `invoice.paid` и остальные. Факт оплаты вы узнаёте из `invoice.paid`, а принадлежность к подписке — из `metadata.subscriptionId`.

## Вопросы и ответы

**Что будет, если покупатель не заплатит?** Счёт просто останется неоплаченным и перейдёт в `expired`. Подписка выставит новый счёт в следующем периоде. Решение отключить доступ к услуге принимаете вы.

**Можно ли поменять сумму?** Да, через `PATCH`. Новая сумма применяется со следующего запуска.

**QR или phone — что выбрать?** Для подписки чаще удобнее `phone`: счёт приходит прямо в приложение Kaspi. Разбор различий: [QR-счёт или счёт по телефону](/kb/ru/qr-vs-phone).

**Можно ли вернуть деньги по подписке?** Да, как обычно — возврат делается по конкретному счёту: [API возвратов](/kb/ru/refunds-api).

**Можно ли всё это проверить в песочнице?** Да. Создайте подписку, вызовите `run` для внеочередного счёта и симулируйте оплату.
