Подписки ApiPay: есть ли автосписание с покупателя?

Обновлено 30 августа 2026 · Справочник · Версия в Markdown
Содержание
  1. Как работает подписка
  2. Создание подписки через API
  3. Что происходит, когда покупатель не платит?
  4. События subscription.* для интеграции
  5. Вопросы и ответы

Как работает подписка

Цикл выглядит так:

  1. Вы создаёте подписку: номер телефона покупателя + период + сумма.
  2. В расчётную дату система сама создаёт обычный счёт Kaspi — покупателю приходит push в приложение Kaspi.
  3. Покупатель нажимает «Оплатить» — деньги, как всегда, идут напрямую на ваш Kaspi-счёт. Вам приходит вебхук subscription.payment_succeeded.
  4. Дата следующего выставления сдвигается на период — и так по кругу.

Создание подписки через 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,
    "billing_time": "13:00",
    "total_cycles": 12,
    "description": "Абонемент, тариф Стандарт"
  }'

Параметры и границы: billing_perioddaily | weekly | biweekly | monthly | quarterly | yearly; amount — от 100 до 1 000 000 ₸ и только целые тенге (дробную сумму создание подписки примет, но каждое списание уйдёт в error с amount_must_be_whole_tenge); max_retry_attempts — 1–10 (по умолчанию 3); retry_interval_hours — 1–168 (по умолчанию 24); grace_period_days — 1–30 (по умолчанию 3); description — до 60 символов (Kaspi показывает покупателю только их); bill_immediately — выставить первый счёт сразу при создании.

Когда именно выставляется счёт

billing_day — 1–28 у месячного, квартального и годового периода; у weekly и biweekly это день недели: 1 — понедельник, 7 — воскресенье (значение больше 7 на этих периодах вернёт 422). У daily поле не используется.

Числом больше 28 день не задаётся: 29-е, 30-е и 31-е есть не в каждом месяце. Если нужен конец месяца, передайте billing_day_from_end вместо billing_day0 означает последний день месяца, 1 — предпоследний. Поля взаимоисключающие: вместе они вернут 422, а сама опора от конца месяца доступна только месячному, квартальному и годовому периоду. В кабинете обе опоры выбираются одним списком, словами: числа с 1-го по 27-е, «предпоследний день месяца» и «последний день месяца». 28-е число доступно только через API.

billing_time — час выставления в формате ЧЧ:ММ по времени Алматы, окно с 06:00 до 22:00. Не передан — счёт уходит в 13:00.

first_billing_at (ГГГГ-ММ-ДД, календарь Алматы) задаёт дату первого счёта, дальше подписка идёт по расписанию. Поле несовместимо с bill_immediately: вместе они вернут 422. Дата не может быть в прошлом и дальше двух лет вперёд — иначе 422. Без него первый счёт считается как «дата начала + период».

total_cycles (1–600) ограничивает подписку по числу оплат: неоплаченная попытка цикл не расходует. Сколько уже внесено, показывает cycles_paid. Поле не передано — подписка бессрочна. Когда оплаты закончились, подписка переходит в expired, а в вебхуке subscription.expired приходит reason: total_cycles_reached.

В ответе подписки next_billing_in_daysзнаковое: отрицательное значение означает, что дата списания уже прошла.

Важно: ответ 201 на POST /subscriptions означает «подписка создана», а не «покупателю уже ушёл счёт». Если вы ждёте немедленный push покупателю — передайте bill_immediately: true. Управление: POST /subscriptions/{id}/pause | resume | cancel, счета подписки — GET /subscriptions/{id}/invoices.

Подпискам нужна верифицированная организация: без неё API отвечает 400 Subscriptions require a verified organization.

Что происходит, когда покупатель не платит?

  1. Счёт не оплачен (истёк через 24 часа или отменён) → вам приходит subscription.payment_failed с причиной (Invoice expired / Invoice cancelled) и номером попытки.
  2. Система сама перевыставляет счёт — по умолчанию до 3 попыток с интервалом retry_interval_hours (по умолчанию 24 ч). Исключение одно: если счёт истёк, он перевыставляется сразу — покупатель уже израсходовал весь срок его жизни, и ждать сверх этого нечего. Вручную ничего пересоздавать не нужно (и не надо: получите два параллельных счёта).
  3. Попытки исчерпаны → начинается grace-период (subscription.grace_period_started, по умолчанию 3 дня): новые счета в это время не выставляются, доступ покупателю вы пока не отключаете, а любая его оплата немедленно возвращает подписку в норму.
  4. Grace истёкsubscription.expired: биллинг по этой подписке остановлен навсегда.

Отдельно от этой лестницы стоит явный отказ покупателя. Если он сам отклонил счёт в Kaspi, подписка отменяется сразу: приходит subscription.cancelled с reason: payer_refused, ретраев и grace не будет. Нехватка денег на счёте отказом не считается — там идут обычные повторы. Отменённую так подписку возобновить нельзя, как и после cancel: если покупатель передумал, создайте новую.

После 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 истёк либо закончились оплаты total_cycles во втором случае reason: total_cycles_reached
subscription.cancelled Отмена: вашим запросом либо явным отказом покупателя при отказе покупателя — reason: payer_refused, invoice_id
subscription.paused / resumed Соответствующие действия

Три инженерных нюанса (трек D):

  • Дедупликация обязательна: у событий подписок нет защиты от дублей — дедуплицируйте по (event, subscription.id, invoice_id).
  • Ретраев в логах не ищите: события subscription.* не попадают в webhook-логи кабинета и не имеют ручного перезапуска — отвечайте 200 быстро и обрабатывайте асинхронно.
  • Счёт подписки в статусе error — это тоже неуспешная попытка: приходит subscription.payment_failed, в payload добавляется error_code. Если ошибка на стороне сервиса (например, оборвалась Kaspi-сессия), попытка не засчитывается и период сохраняется — следующая попытка будет позже. Если причина в плательщике (client_not_found — у номера нет Kaspi), идут обычные ретраи и затем grace.
  • Позиция каталога, создание которой брошено, ломает списание. Если в корзине подписки стоит позиция с sellable: false (status: failed при operation: create), счёт не выставится, и прогон засчитается неудачным: failed_attempts растёт, дальше ретраи, grace и expired. Это не отсрочка — в отличие от неоплаченного тарифа, где цикл просто пропускается. Позицию нужно починить до исчерпания max_retry_attempts: работу по строке возобновляет PATCH /catalog/{id}, в кабинете — кнопка «Повторить».

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

Почему нельзя сделать настоящее автосписание?

Оплата по номеру телефона в Kaspi всегда подтверждается самим покупателем: списание без подтверждения доступно только в карточном эквайринге с токенизацией карты. Клиентам это честно формулируется так: «счёт будет приходить автоматически, оплата в один клик».

Можно ли изменить сумму действующей подписки?

Да, через PUT /subscriptions/{id} — изменения применяются к будущим счетам. Тем же методом меняются день и час списания и число оплат; дата первого счёта и bill_immediately задаются только при создании.

Во сколько покупателю приходит счёт по подписке?

По умолчанию счета уходят в 13:00 по времени Алматы. Час выбирается полем billing_time в окне с 06:00 до 22:00.

Подписка умерла (expired) — я потеряю историю?

Нет: подписка и все её счета остаются в кабинете и API. Остановлен только будущий биллинг — для продолжения создайте новую подписку.

Интеграция с ИИ

Не настраивайте вручную — поручите ИИ

Скопируйте готовый промпт и вставьте своему ИИ-ассистенту — он настроит интеграцию за вас: получит доступы, создаст тестовый счёт в песочнице и подключит вебхуки. Техническую часть делает ИИ, а не вы.

Остались вопросы — напишите нам в WhatsApp: +7 700 307 65 12. Отвечаем быстро, без звонков.

Написать в WhatsApp

Для разработчика. Полная спецификация API — apipay.kz/docs. Эта статья в Markdown — /guides/podpiski-apipay.md. Справочник фактов для ИИ-агента — apipay.kz/llms.txt.