# Онлайн-школа и платформа курсов

> Продажа доступа к курсу, открытие доступа по webhook, периодические платежи, групповые тарифы и скидки. Плюс правило App Store и Google Play, если у школы есть мобильное приложение.

## Коротко

В онлайн-школе всё сводится к одной цепочке: **счёт → оплата → webhook → доступ**. Ученик выбирает курс, вы создаёте счёт, он платит через Kaspi, вам приходит `invoice.paid`, и ваша платформа в тот же момент открывает курс. Для периодических платежей есть [подписки](/kb/ru/subscriptions-api) — они выставляют счёт по расписанию, но деньги сами не списываются: каждую оплату ученик подтверждает в Kaspi лично.

## Базовый сценарий: покупка доступа

```json
POST /api/v1/invoices
{
  "amount": 39000,
  "kind": "qr",
  "description": "Курс «Основы Python», поток 2",
  "externalOrderId": "ENR-8842",
  "customer": { "name": "Айдана", "email": "aidana@example.kz" },
  "successUrl": "https://school.kz/enroll/8842/done",
  "failUrl": "https://school.kz/enroll/8842/retry",
  "metadata": { "user_id": "3311", "course_id": "py-basics", "cohort": "2" }
}
```

- `successUrl` и `failUrl` — только http(s). После оплаты покупатель возвращается на эту страницу.
- **Не считайте `successUrl` признаком оплаты.** Это просто страница, её можно открыть вручную. Настоящее подтверждение приходит только через webhook.
- В `metadata` держите `user_id` и `course_id` — по ним вы поймёте, кому и что открывать, когда вернётся webhook.
- Отправляйте заголовок `Idempotency-Key`: если ученик дважды нажал «Оплатить», второй счёт не создастся.

Когда ученик на сайте, удобнее `kind: "qr"`: он сканирует QR со страницы телефоном или переходит по `payUrl`. Если же вы переписываетесь в WhatsApp или Instagram, лучше `kind: "phone"` — push придёт прямо в Kaspi, но учтите, что `description` там не длиннее 60 символов.

## Открытие доступа по webhook

Это самая ответственная часть. Порядок такой:

1. В кабинете, в разделе **Интеграции**, добавляете адрес webhook. В бою принимается только `https` и реальный домен: IP и адреса туннелей не подойдут.
2. Секрет показывается один раз — сохраните его.
3. Адрес должен быть открыт без авторизации. Редиректы на другой адрес не отслеживаются.
4. Проверяете подпись: `HMAC-SHA256(secret, timestamp + "." + rawBody)`, hex, с префиксом `sha256=`. Тело — **в неизменном байтовом виде**, до разбора JSON. `timestamp` старше 5 минут принимать не надо.
5. По `invoice.paid` открываете курс, находя ученика по `metadata.user_id` и `metadata.course_id`.
6. Отвечаете 2xx. Если не ответили — доставка повторится до **11 раз** (с нарастанием от 10 секунд до часа).

Обработчик должен быть идемпотентным: храните пару `(invoice.id, status)` и игнорируйте повторы. Иначе одному ученику уйдёт одиннадцать писем «добро пожаловать».

Подробнее: [Настройка вебхуков](/kb/ru/webhook-setup), [Безопасность вебхуков](/kb/ru/webhook-security), [Вебхук не приходит](/kb/ru/webhook-not-arriving).

## Периодические платежи

Ежемесячная подписка, оплата обучения траншами, формат «курс + сопровождение» — всё это [подписки](/kb/ru/subscriptions-api).

- **Интервал**: `month` (обычный случай), `week`, `day`; кратность задаётся через `every`.
- **Шаги повтора** `retryDelaysMin`, по умолчанию `[15, 60, 360]` минут, максимум 5 значений. Если ученик не увидел счёт сразу, он выставится снова.
- Когда шаги закончились, этот период отбрасывается, расписание идёт дальше. Всё видно в `failedRuns`, `lastError`, `lastRunStatus`.
- **Политика пропуска** `misfirePolicy`: `run_once` (по умолчанию) или `skip`, `misfireAfterMin` по умолчанию 1440.
- Ученик ушёл на каникулы — `pause`, вернулся — `resume`. Нужно сразу догнать пропущенный запуск: `POST /subscriptions/{id}/resume` с `{ catchUp: true }`.

**Закрывать доступ решаете вы.** Если подписка не оплачена, курс мы не закрываем — мы лишь выставили счёт и сообщили, что он не оплачен. Ваша платформа видит неоплаченный период и сама решает, закрыть доступ или оставить.

Подробнее: [Бизнес по подписке](/kb/ru/for-subscription-business).

## Групповые тарифы и скидки

**Скидки.** Сумму считаете вы. Промокод, цена раннего бронирования, социальная скидка — всё это решается на вашей стороне, а в счёт уходит уже готовая сумма. Qut Pay скидки не считает, системы промокодов у нас нет.

**Групповой тариф.** Компания отправляет сотрудников на обучение или группа родителей записывается вместе — есть два пути:

| Путь | Когда удобно | Как |
|---|---|---|
| Один общий счёт | Платит компания | Один счёт на всю сумму, список в `metadata` |
| Отдельный счёт каждому | Платит каждый сам | [Массовое создание](/kb/ru/bulk-invoices): `POST /invoices/bulk`, от 1 до 100 за запрос |

При массовом создании каждый элемент проверяется отдельно: ошибка в одном не мешает остальным создаться. Разберите ответ поэлементно и переотправьте только упавшие.

## Если у школы есть мобильное приложение

Здесь нужна осторожность. **По правилам App Store и Google Play цифровые товары нельзя продавать через внешнюю оплату.** Доступ к курсу, подписка, премиум внутри платформы — это цифровые товары. Реальный товар, доставка, офлайн-услуга и бронирование — можно.

На практике большинство онлайн-школ делает так: **оплата происходит на сайте, в браузере**, а приложение показывает уже открытый доступ. Ссылка на оплату прямо из приложения тоже может нарушать правила магазинов — прочитайте их сами и оцените собственный риск.

Техническая сторона отдельная: приложение **не обращается к нам напрямую**. Порядок такой: приложение → ваш сервер → Qut Pay → Kaspi. API-ключ должен жить только на сервере, класть его в APK или IPA нельзя. Подробнее: [Приём оплаты в мобильном приложении](/kb/ru/for-mobile-app).

## Вариант без кода

- **Постоянные ссылки на оплату.** По ссылке на каждый курс: `qut.kz/p/<slug>`. Их кладут в профиль Instagram, в Telegram-канал, на лендинг. Оплату видите в кабинете и открываете доступ вручную: [Ссылки на оплату](/kb/ru/payment-links).
- **Tilda и любые формы.** Счёт из формы записи выставляется через хук формы: [Tilda и любые формы](/kb/ru/tilda-forms).
- **Если сайт на WordPress**, есть плагин: [Плагин WooCommerce](/kb/ru/woocommerce).
- **Продажа через Telegram-бота.** Курс продаётся в боте, а после оплаты бот отправляет приглашение в закрытый канал: [Продажи через Telegram-бота](/kb/ru/for-telegram-bot).
- **n8n.** Есть готовые сценарии на создание счёта и приём webhook.

## Что учесть заранее

- **Доступ открывает только `invoice.paid`.** `invoice.created` и `invoice.pending` оплатой не являются.
- **Поздние оплаты.** Деньги могут прийти уже после того, как счёт стал `expired`, и `invoice.paid` придёт с пометкой `late: true`. Либо открываете курс, либо возвращаете деньги: [Поздняя оплата](/kb/ru/late-payment).
- **Не кладите API-ключ во фронтенд.** Только сервер. Ключ в браузерном JS считайте утёкшим: [API-ключ утёк](/kb/ru/leaked-key).
- **Готовьтесь к пикам продаж.** В день открытия потока число счетов вырастает в разы. Месячный лимит и суточная защита — это разные вещи: [Тарифы и лимиты](/kb/ru/tariff-limits).
- **Заранее объясните ученикам свою политику возврата.** Технически доступен и полный, и частичный: [API возвратов](/kb/ru/refunds-api).

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

**Курс откроется сразу после оплаты?** Да, webhook `invoice.paid` обычно приходит в пределах 5 секунд, и платформа открывает доступ в тот же момент.

**Ученик оплатил, а доступ не открылся. Что проверять?** Сначала журнал webhook: доступен ли адрес, ответили ли вы 2xx, сошлась ли подпись. Порядок проверки: [Вебхук не приходит](/kb/ru/webhook-not-arriving).

**Можно сделать промокоды?** На стороне Qut Pay системы промокодов нет. Скидку считает ваша платформа и передаёт в счёт готовую сумму.

**Если подписка не оплачена, вы закроете доступ к курсу?** Нет. Мы выставляем счёт и сообщаем его статус, а закрывать доступ или нет — решает ваша платформа.

**Можно принимать оплату прямо в приложении?** Технически да, но через ваш сервер. При этом правила App Store и Google Play запрещают продавать цифровые товары через внешнюю оплату, поэтому для онлайн-курсов безопаснее выносить оплату в браузер: [Приём оплаты в мобильном приложении](/kb/ru/for-mobile-app).
