Коротко
Подписка — это автоматическое выставление счетов по расписанию. Деньги с карты покупателя сами не уходят: в каждом периоде создаётся новый счёт, он приходит покупателю, и каждую оплату покупатель подтверждает сам. Автоматизируется у вас другое — своевременный выпуск счёта и повтор неудачных попыток.
Минимальный пример:
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 есть важный нюанс:
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 сразу называют причину. Подробный разбор: Подписка не выставляет счета.
Кассир
Подписка запоминает, через какого кассира она создана, и дальше выставляет счета через него же. Отсюда два следствия:
- Если привязка этого кассира оборвалась, счета подписки падают с
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-счёт или счёт по телефону.
Можно ли вернуть деньги по подписке? Да, как обычно — возврат делается по конкретному счёту: API возвратов.
Можно ли всё это проверить в песочнице? Да. Создайте подписку, вызовите run для внеочередного счёта и симулируйте оплату.