Как работает подписка
Цикл выглядит так:
- Вы создаёте подписку: номер телефона покупателя + период + сумма.
- В расчётную дату система сама создаёт обычный счёт Kaspi — покупателю приходит push в приложение Kaspi.
- Покупатель нажимает «Оплатить» — деньги, как всегда, идут напрямую на ваш Kaspi-счёт. Вам приходит вебхук
subscription.payment_succeeded. - Дата следующего выставления сдвигается на период — и так по кругу.
Создание подписки через API
curl -X POST "https://api.apipay.kz/api/v1/subscriptions" \
-H "X-API-Key: ВАШ_КЛЮЧ" \
-H "Content-Type: application/json" \
-d '{
"phone_number": "87001234567",
"amount": 15000,
"billing_period": "monthly",
"billing_day": 1,
"description": "Абонемент, тариф Стандарт"
}'
Параметры и границы: billing_period — daily | weekly | biweekly | monthly | quarterly | yearly; amount — от 100 до 1 000 000 ₸ и только целые тенге (дробную сумму создание подписки примет, но каждое списание уйдёт в error с amount_must_be_whole_tenge); billing_day — 1–28 (чтобы дата существовала в любом месяце); max_retry_attempts — 1–10 (по умолчанию 3); retry_interval_hours — 1–168 (по умолчанию 24); grace_period_days — 1–30 (по умолчанию 3); bill_immediately — выставить первый счёт сразу при создании.
Важно: ответ 201 на POST /subscriptions означает «подписка создана», а не «покупателю уже ушёл счёт». Если вы ждёте немедленный push покупателю — передайте bill_immediately: true. Управление: POST /subscriptions/{id}/pause | resume | cancel, счета подписки — GET /subscriptions/{id}/invoices.
Подпискам нужна верифицированная организация: без неё API отвечает 400 Subscriptions require a verified organization.
Что происходит, когда покупатель не платит?
- Счёт не оплачен (истёк через 24 часа или отменён) → вам приходит
subscription.payment_failedс причиной (Invoice expired/Invoice cancelled) и номером попытки. - Система сама перевыставляет счёт — по умолчанию до 3 попыток.
retry_interval_hours(по умолчанию 24 ч) — номинальный интервал: фактически повтор может прийти раньше, поэтому не стройте дедуп и напоминания клиенту строго по 24-часовой сетке. Вручную ничего пересоздавать не нужно (и не надо: получите два параллельных счёта). - Попытки исчерпаны → начинается grace-период (
subscription.grace_period_started, по умолчанию 3 дня): доступ покупателю вы пока не отключаете, а любая его оплата немедленно возвращает подписку в норму. - Grace истёк →
subscription.expired: биллинг по этой подписке остановлен навсегда.
После expired реактивации не существует — ни кнопки, ни метода API. Покупатель вернулся через месяц? Создайте новую подписку.
Отдельная причина тишины — ваш собственный тариф ApiPay. Если тариф не оплачен (403 tariff_inactive) или включено ограничение по дневному лимиту (429 tariff_limit_reached), автосписание пропускает цикл молча: счёт покупателю не выставляется, дата следующего списания не сдвигается, счётчик неудачных попыток не растёт, вебхука об этом нет. После оплаты тарифа или снятия ограничения подписки догоняют пропущенные периоды — по одному за прогон. См. Лимит счетов по тарифу.
Пауза устроена мягче: pause останавливает выставление, resume продолжает от текущего момента — пропущенные периоды не доначисляются, покупателю не прилетит «счёт за три месяца тишины». cancel — безвозвратен, как и expired.
События subscription.* для интеграции
Вебхуки подписок приходят на тот же URL, что и счета (настройка — «Настройка вебхуков ApiPay»):
| Событие | Когда | Доп. поля (в корне payload) |
|---|---|---|
subscription.created |
Подписка создана | — |
subscription.payment_succeeded |
Счёт подписки оплачен | invoice_id, amount, paid_at |
subscription.payment_failed |
Счёт истёк/отменён | invoice_id, amount, reason, attempt_number |
subscription.grace_period_started |
Ретраи исчерпаны | grace_period_days, expires_at |
subscription.expired |
Grace истёк, биллинг остановлен | — |
subscription.paused / resumed / cancelled |
Соответствующие действия | — |
Три инженерных нюанса (трек D):
- Дедупликация обязательна: у событий подписок нет защиты от дублей — дедуплицируйте по
(event, subscription.id, invoice_id). - Ретраев в логах не ищите: события
subscription.*не попадают в webhook-логи кабинета и не имеют ручного перезапуска — отвечайте200быстро и обрабатывайте асинхронно. - Счёт подписки в статусе
error— это тоже неуспешная попытка: приходитsubscription.payment_failed, в payload добавляетсяerror_code. Если ошибка на стороне сервиса (например, оборвалась Kaspi-сессия), попытка не засчитывается и период сохраняется — следующая попытка будет позже. Если причина в плательщике (client_not_found— у номера нет Kaspi), идут обычные ретраи и затем grace.
Вопросы и ответы
Почему нельзя сделать настоящее автосписание?
Оплата по номеру телефона в Kaspi всегда подтверждается самим покупателем: списание без подтверждения доступно только в карточном эквайринге с токенизацией карты. Клиентам это честно формулируется так: «счёт будет приходить автоматически, оплата в один клик».
Можно ли изменить сумму действующей подписки?
Да, через PUT /subscriptions/{id} — изменения применяются к будущим счетам.
Подписка умерла (expired) — я потеряю историю?
Нет: подписка и все её счета остаются в кабинете и API. Остановлен только будущий биллинг — для продолжения создайте новую подписку.