Courstore API
API школы
Внешнее зачисление учеников на курсы вашей школы из CRM или другой системы. Оплата — на вашей стороне, Courstore открывает доступ и уведомляет ученика.
https://api.courstore.com/v1/school-apiСпецификация в Swagger →Обзор
School API позволяет вашей системе (CRM, платёжный сервис, собственный сайт) зачислять учеников на курсы школы на Courstore и проверять их доступ. Оплату вы принимаете сами — API только открывает доступ к курсу.
- Базовый URL:
https://api.courstore.com/v1/school-api - Формат: JSON. Тело запросов —
Content-Type: application/json, размер до 4 КБ. - Авторизация: ключ школы в заголовке
X-School-Key. - Доступ: тарифы Pro (годовой), Lifetime и Enterprise. На остальных тарифах API отвечает
403 tariff_not_allowed— тариф проверяется на каждом запросе. - Ошибки всегда приходят в виде JSON
{"error": "код", "message": "текст"}.
Полная машиночитаемая спецификация: Swagger.
| Метод | URL | Назначение |
|---|---|---|
| GET | /courses | Список курсов школы и срок доступа по умолчанию |
| POST | /students | Найти ученика по телефону или создать |
| POST | /enrollments | Зачислить ученика на курс (создаёт ученика, если его нет) |
| GET | /enrollments/{requestId} | Статус зачисления по requestId |
| GET | /enrollments?phone=&courseId= | Текущий доступ ученика к курсу |
Быстрый старт
- Проверьте тариф. API доступен на Pro (годовой), Lifetime и Enterprise.
- Получите ключ. В кабинете школы откройте «Интеграции» → «API школы» → «Сгенерировать ключ». Ключ вида
cs_live_…показывается один раз — сразу сохраните его в надёжном месте. Можно держать до 3 активных ключей, любой можно отозвать. - Отправьте первый запрос. Получите список курсов школы — в ответе будут
idкурсов, которые нужны для зачисления.
export KEY="cs_live_..."
export BASE="https://api.courstore.com/v1/school-api"curl -s "$BASE/courses" \
-H "X-School-Key: $KEY"[
{
"id": 1234,
"title": "Основы маркетинга",
"slug": "osnovy-marketinga",
"status": "publish",
"access": {
"mode": "months",
"amount": 3
}
},
{
"id": 1301,
"title": "Мастер-класс",
"slug": "master-klass",
"status": "publish",
"access": {
"mode": "unlimited",
"amount": null
}
}
]Затем зачислите ученика на курс — подробности в методе «Зачислить на курс»:
curl -s -X POST "$BASE/enrollments" \
-H "X-School-Key: $KEY" \
-H "Idempotency-Key: order-0042" \
-H "Content-Type: application/json" \
-d '{"phone":"+77001234567","courseId":1234,"access":{"mode":"days","amount":30},"notify":true,"orderRef":"CRM-0042"}'{
"requestId": "req_882",
"status": "queued",
"studentId": 118350,
"enrollmentId": null,
"expireDate": null,
"lastDay": null
}Ответ 202 со статусом queued означает, что зачисление принято в очередь. Результат — в методе «Статус зачисления» (обычно готово через несколько секунд).
Аутентификация
Каждый запрос должен содержать заголовок X-School-Key с ключом школы:
curl -s "https://api.courstore.com/v1/school-api/courses" \
-H "X-School-Key: cs_live_..."- Ключ создаётся в кабинете: «Интеграции» → «API школы». Формат —
cs_live_…, показывается один раз. - На школу — до 3 активных ключей. Любой ключ можно отозвать в той же карточке; интеграции, которые его используют, перестанут работать.
- Храните ключ на сервере. Не размещайте его в мобильных приложениях, браузерном коде и публичных репозиториях.
- Срок действия необязателен: при выпуске ключа можно указать дату «действует до» (включая этот день). После неё ключ отвечает
401 key_expired— выпустите новый. - Когда тариф школы заканчивается, все её ключи отзываются автоматически и навсегда; после продления тарифа выпустите новый ключ. Отозванный ключ получает
401 bad_key. - Если ключ не передан, неверен или отозван — ответ
401:
{
"error": "bad_key",
"message": "Неверный ключ API"
}Если ключ верный, но тариф школы не даёт доступ к API — 403 tariff_not_allowed. В карточке «API школы» видно использование ключей за сегодня и последние ошибки.
Методы
Во всех примерах ниже используются переменные $BASE и $KEY из раздела «Быстрый старт». Пути указаны относительно базового URL https://api.courstore.com/v1/school-api.
Курсы школы
/coursesВозвращает курсы школы-владельца ключа. Поле access — срок доступа по умолчанию из настроек курса (используется, если при зачислении access не передан).
Параметров нет.
Пример запроса
curl -s "$BASE/courses" \
-H "X-School-Key: $KEY"Поля ответа
| Поле | Тип | Описание |
|---|---|---|
id | integer | Идентификатор курса — его передают в courseId |
title | string | Название курса |
slug | string | Короткое имя курса в адресе |
status | string | publish | private | draft | pending. Зачислять можно только на опубликованный курс |
access.mode | string | days | months | unlimited |
access.amount | integer | null | Количество дней или месяцев; null при unlimited |
Пример ответа
[
{
"id": 1234,
"title": "Основы маркетинга",
"slug": "osnovy-marketinga",
"status": "publish",
"access": {
"mode": "months",
"amount": 3
}
},
{
"id": 1301,
"title": "Мастер-класс",
"slug": "master-klass",
"status": "publish",
"access": {
"mode": "unlimited",
"amount": null
}
}
]Ошибки метода
| HTTP | Код | Что значит |
|---|---|---|
| 401 | bad_key | Ключ не передан, неверен или отозван |
| 401 | key_expired | У ключа истёк срок действия |
| 403 | tariff_not_allowed | Тариф школы не даёт доступ к API |
| 429 | rate_limited | Превышен лимит 60 запросов в минуту |
Найти или создать ученика
/studentsИщет ученика по телефону и возвращает его; если такого ученика в вашей школе нет — создаёт. Телефон нормализуется (+7 / 8 / пробелы / скобки). Имя и почта существующего ученика не перезаписываются. Новый ученик создаётся без пароля — вход по коду.
Тело запроса (JSON)
| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
phone | string | да | Телефон в любом формате |
name | string | нет | Имя, до 200 символов |
email | string | нет | Почта, до 200 символов |
lang | string | нет | Язык ученика: ru | kk | en | uz. По умолчанию ru |
Пример запроса
curl -s -X POST "$BASE/students" \
-H "X-School-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"phone":"+7 (700) 123-45-67","name":"Анна","email":"anna@example.com","lang":"ru"}'Поля ответа
| Поле | Тип | Описание |
|---|---|---|
studentId | integer | Идентификатор ученика |
created | boolean | true — ученика ещё не было в вашей школе (ответ 201); false — уже учится (ответ 200) |
Пример ответа
{
"studentId": 118350,
"created": true
}{
"studentId": 118347,
"created": false
}Ошибки метода
| HTTP | Код | Что значит |
|---|---|---|
| 401 | bad_key | Ключ не передан, неверен или отозван |
| 401 | key_expired | У ключа истёк срок действия |
| 403 | tariff_not_allowed | Тариф школы не даёт доступ к API |
| 409 | student_limit_reached | Достигнут лимит студентов по тарифу |
| 422 | validation_error | Некорректные данные, например номер телефона; также invalid_json и payload_too_large |
| 429 | rate_limited | Превышен лимит 60 запросов в минуту |
Зачислить на курс
/enrollmentsЗачисляет ученика на курс. Если ученика с таким телефоном нет — он создаётся. Срок доступа берётся из access, если он передан, иначе из настроек курса. Заголовок Idempotency-Key обязателен.
Заголовок и тело запроса
| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
Idempotency-Key | header, string | да | Уникальный ключ операции (например, номер заказа в CRM), до 128 символов: буквы, цифры, . _ : - |
phone | string | да | Телефон ученика в любом формате |
courseId | integer | да | Идентификатор курса из GET /courses |
name | string | нет | Имя, до 200 символов (используется для нового ученика) |
email | string | нет | Почта, до 200 символов |
access | object | нет | Срок доступа. Если не передан — из настроек курса |
access.mode | string | нет | days | months |
access.amount | integer | нет | days: 1…3650; months: 1…120 |
notify | boolean | нет | Отправлять ученику уведомления о зачислении. По умолчанию true |
orderRef | string | нет | Ваш номер заказа, до 128 символов (для вашего учёта) |
Пример запроса
curl -s -X POST "$BASE/enrollments" \
-H "X-School-Key: $KEY" \
-H "Idempotency-Key: order-0042" \
-H "Content-Type: application/json" \
-d '{"phone":"+77001234567","name":"Анна","courseId":1234,"orderRef":"CRM-2026-0042"}'Поля ответа
| Поле | Тип | Описание |
|---|---|---|
requestId | string | Идентификатор запроса — для GET /enrollments/{requestId} |
status | string | queued — принято в очередь; done — выполнено |
studentId | integer | null | Идентификатор ученика |
enrollmentId | integer | null | Идентификатор зачисления; null, пока запрос в очереди |
expireDate | date | null | Граница закрытия доступа; null — бессрочно или ещё не известна |
lastDay | date | null | Последний день доступа |
Пример ответа
{
"requestId": "req_882",
"status": "queued",
"studentId": 118350,
"enrollmentId": null,
"expireDate": null,
"lastDay": null
}{
"requestId": "req_881",
"status": "done",
"studentId": 118347,
"enrollmentId": 902113,
"expireDate": "2026-12-01",
"lastDay": "2026-11-30"
}Новое зачисление — ответ 202, status: queued. Зачисление и уведомления выполняются в течение примерно минуты (на практике — секунды); результат — в GET /enrollments/{requestId}.
Повторная покупка (ученик уже зачислен на курс) — доступ продлевается сразу, ответ 200, status: done.
Ошибки метода
| HTTP | Код | Что значит |
|---|---|---|
| 401 | bad_key | Ключ не передан, неверен или отозван |
| 401 | key_expired | У ключа истёк срок действия |
| 403 | tariff_not_allowed | Тариф школы не даёт доступ к API |
| 404 | course_not_found | Курс не найден в вашей школе (в том числе чужой курс) |
| 409 | idempotency_conflict | Этот Idempotency-Key уже использован с другим телом |
| 409 | idempotency_in_progress | Запрос с этим ключом ещё выполняется |
| 409 | course_locked | Курс заморожен |
| 409 | course_closed | Курс не опубликован (например, черновик) |
| 409 | student_limit_reached | Достигнут лимит студентов по тарифу |
| 422 | idempotency_key_required | Не передан заголовок Idempotency-Key; также validation_error, invalid_json, payload_too_large |
| 429 | rate_limited | Превышен лимит 60 запросов в минуту |
| 429 | daily_limit | Превышен лимит 300 зачислений в сутки на школу |
Статус зачисления
/enrollments/{requestId}Возвращает состояние зачисления, принятого методом POST /enrollments.
Параметры пути
| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
requestId | string | да | Идентификатор из ответа на зачисление, например req_882 |
Пример запроса
curl -s "$BASE/enrollments/req_882" \
-H "X-School-Key: $KEY"Поля ответа
| Поле | Тип | Описание |
|---|---|---|
requestId | string | Идентификатор запроса |
status | string | queued — ждёт обработки; done — ученик зачислен; failed — за 15 минут зачисление не подтвердилось |
studentId | integer | null | Идентификатор ученика |
courseId | integer | null | Идентификатор курса |
expireDate | date | null | Граница закрытия доступа |
lastDay | date | null | Последний день доступа |
notified | boolean | true — уведомление ученику поставлено в отправку (доставку сервис не подтверждает) |
error | string | null | Причина при failed: not_confirmed |
Пример ответа
{
"requestId": "req_882",
"status": "done",
"studentId": 118350,
"courseId": 1234,
"expireDate": "2027-01-20",
"lastDay": "2027-01-19",
"notified": true,
"error": null
}Ошибки метода
| HTTP | Код | Что значит |
|---|---|---|
| 401 | bad_key | Ключ не передан, неверен или отозван |
| 401 | key_expired | У ключа истёк срок действия |
| 403 | tariff_not_allowed | Тариф школы не даёт доступ к API |
| 404 | — | Запрос с таким requestId не найден |
| 429 | rate_limited | Превышен лимит 60 запросов в минуту |
Проверка доступа
/enrollments?phone=&courseId=Показывает текущий доступ ученика к курсу вашей школы. Ученик, не связанный с вашей школой, отвечает так же, как несуществующий: active: false.
Параметры запроса (query)
| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
phone | string | да | Телефон ученика. Знак + в URL передавайте как %2B |
courseId | integer | да | Идентификатор курса |
Пример запроса
curl -s "$BASE/enrollments?phone=%2B77001234567&courseId=1234" \
-H "X-School-Key: $KEY"Поля ответа
| Поле | Тип | Описание |
|---|---|---|
active | boolean | Есть ли у ученика действующий доступ |
expireDate | date | null | Граница закрытия доступа |
lastDay | date | null | Последний день доступа |
progress | integer | Прогресс по курсу, % |
Пример ответа
{
"active": true,
"expireDate": "2026-12-01",
"lastDay": "2026-11-30",
"progress": 35
}Ошибки метода
| HTTP | Код | Что значит |
|---|---|---|
| 401 | bad_key | Ключ не передан, неверен или отозван |
| 401 | key_expired | У ключа истёк срок действия |
| 403 | tariff_not_allowed | Тариф школы не даёт доступ к API |
| 404 | course_not_found | Курс не найден в вашей школе |
| 422 | validation_error | Не переданы или некорректны phone / courseId |
| 429 | rate_limited | Превышен лимит 60 запросов в минуту |
Ссылка входа для ученика
Ваш сервис может выдать ученику одноразовую ссылку, которая откроет кабинет вашей школы на Courstore без пароля. Токен в ссылке — 32 случайных байта, у нас хранится только хеш, после первого использования ссылка гаснет.
Ссылка входа в Courstore
Ученик уже вошёл в ваш сервис — дайте ему ссылку, которая откроет кабинет вашей школы на Courstore без пароля. Ссылка одноразовая.
/students/login-linkТело запроса
| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
phone | string | да | Телефон ученика в любом формате |
redirect | integer | нет | id курса вашей школы: после входа откроется страница курса. Без него — «Мои курсы» |
ttlSeconds | integer | нет | Срок жизни ссылки, от 30 до 86400 секунд, по умолчанию 120 |
Пример запроса
curl -s -X POST "$BASE/students/login-link" \
-H "X-School-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"phone":"+77001234567","redirect":1234,"ttlSeconds":120}'Пример ответа
{
"url": "https://myschool.courstore.com/ru/login-via-token?token=csl_...&returnUrl=courses%2Fosnovy-marketinga",
"expiresAt": "2026-10-16T10:02:00Z"
}expiresAt — до какого момента (UTC) ссылка действует.
Какой срок выбрать
| Сценарий | ttlSeconds | Почему |
|---|---|---|
| Кнопка «Перейти в Courstore» на вашем сайте: получили ссылку и сразу сделали редирект | 120 (по умолчанию) | Ссылка нужна несколько секунд; короткий срок — меньше риск, если адрес куда-то утечёт |
| Ссылка в письме, WhatsApp или Telegram | до 86400 (24 ч) | Ученик может открыть сообщение не сразу. Ссылка всё равно сработает только один раз |
curl -s -X POST "$BASE/students/login-link" \
-H "X-School-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"phone":"+77001234567","ttlSeconds":86400}'Если ссылка устарела или её уже открыли, ученик не увидит ошибку: он попадёт на обычную страницу входа вашей школы с подсказкой «Ссылка устарела — войдите по номеру телефона».
Пример на сервере
app.get("/go-to-courstore", requireLogin, async (req, res) => {
const r = await fetch(`${API}/students/login-link`, {
method: "POST",
headers: { "X-School-Key": KEY, "Content-Type": "application/json" },
body: JSON.stringify({ phone: req.user.phone, redirect: 1234 }),
});
if (r.status !== 200) return res.redirect("https://myschool.courstore.com/ru/login");
const { url } = await r.json();
res.redirect(url);
});Ошибки метода
| HTTP | Код | Что значит |
|---|---|---|
| 404 | student_not_found | Номер не принадлежит ученику вашей школы. Автору, сотрудникам и администраторам ссылки входа не выдаются никогда — для них тоже этот ответ |
| 404 | course_not_found | Курс из redirect не принадлежит вашей школе |
| 422 | validation_error | Некорректный phone, redirect или ttlSeconds вне 30…86400 |
| 429 | daily_limit | Больше 2000 ссылок в сутки на школу; см. заголовок Retry-After |
| 429 | rate_limited | Больше 60 запросов в минуту на ключ (общий лимит API) |
Безопасность. Ссылка равна входу в аккаунт ученика: не пишите её в логи, не пересылайте третьим лицам и выдавайте только тому ученику, который вошёл в ваш сервис.
Вебхуки
Вебхуки сообщают вашей системе о зачислениях и изменениях доступа сразу, без опроса API: Courstore сам отправляет POST на ваш адрес.
Настройка
- В кабинете: «Интеграции» → «API школы» → «Вебхуки» → URL. Только
https://и только публичный адрес: внутренние и локальные адреса и IP-адреса запрещены. - При первом сохранении выдаётся секрет подписи
whsec_…. Он показывается один раз — сохраните его сразу. Секрет можно перевыпустить: старый сразу перестаёт работать. - Кнопка «Отправить тест» шлёт событие
webhook.test. В журнале видны последние 20 доставок: событие, код ответа, попытка, время.
События
Тип события приходит в поле type и в заголовке X-Courstore-Event.
type | Когда приходит | accessUntil |
|---|---|---|
enrollment.created | Ученик зачислен на курс: оплата, ручное зачисление в кабинете, School API | Последний день доступа или null |
access.extended | Доступ продлён или курс куплен повторно | Новый последний день |
access.expired | Срок доступа закончился. Приходит только для окончаний после включения вебхука | Последний день доступа |
access.revoked | Автор закрыл ученику доступ к курсу | Поля нет |
webhook.test | Кнопка «Отправить тест» в кабинете | — |
accessUntil — последний день доступа включительно (YYYY-MM-DD), null — бессрочно.
Тело запроса
{
"id": "evt_8f3a1c0e2b7d4a91",
"type": "enrollment.created",
"createdAt": "2026-10-16T10:00:00Z",
"schoolId": 7,
"data": {
"enrollmentId": 501,
"student": {
"id": 118347,
"name": "Анна",
"phone": "+77001234567",
"email": null
},
"course": {
"id": 1234,
"title": "Основы маркетинга"
},
"accessUntil": "2026-12-31"
}
}{
"id": "evt_1b9e44d07c2f8e30",
"type": "access.revoked",
"createdAt": "2026-10-20T08:15:00Z",
"schoolId": 7,
"data": {
"enrollmentId": 501,
"student": {
"id": 118347,
"name": "Анна",
"phone": "+77001234567",
"email": null
},
"course": {
"id": 1234,
"title": "Основы маркетинга"
}
}
}Поля ответа
| Поле | Тип | Описание |
|---|---|---|
id | string | Уникальный id события evt_…; совпадает с X-Courstore-Delivery. Ключ идемпотентности |
type | string | Тип события |
createdAt | date-time | Когда произошло событие (UTC) |
schoolId | integer | id вашей школы на Courstore |
data.enrollmentId | integer | id зачисления |
data.student | object | id, name, phone, email ученика |
data.course | object | id и title курса |
data.accessUntil | date | null | Последний день доступа; null — бессрочно. В access.revoked поля нет |
Заголовки и подпись
| Заголовок | Описание |
|---|---|
X-Courstore-Event | Тип события |
X-Courstore-Delivery | id события (то же, что id в теле) — для защиты от повторов |
X-Courstore-Signature | t=<unix-время>,v1=<hex>, где v1 = HMAC-SHA256(секрет, t + "." + сырое тело) |
POST /courstore/webhook HTTP/1.1
Content-Type: application/json
X-Courstore-Event: enrollment.created
X-Courstore-Delivery: evt_8f3a1c0e2b7d4a91
X-Courstore-Signature: t=1792144800,v1=5f0c2b…- Возьмите сырое тело запроса — до разбора JSON: любое переформатирование ломает подпись.
- Посчитайте HMAC-SHA256 секретом
whsec_…от строкиt + "." + тело, в hex. - Сравните с
v1сравнением за постоянное время (timingSafeEqual,hash_equals,compare_digest). - Отклоняйте запросы, у которых
tстарше 5 минут, — это защита от повторной отправки перехваченного запроса.
Пример проверки подписи
const crypto = require("crypto");
const express = require("express");
const app = express();
app.post("/courstore/webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = Object.fromEntries(
String(req.get("X-Courstore-Signature") || "").split(",").map((p) => p.split("="))
);
const expected = crypto
.createHmac("sha256", process.env.COURSTORE_WEBHOOK_SECRET)
.update(sig.t + "." + req.body.toString("utf8"))
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(sig.t)) < 300;
const ok = fresh && typeof sig.v1 === "string" && sig.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig.v1));
if (!ok) return res.sendStatus(401);
const event = JSON.parse(req.body.toString("utf8"));
// event.id — idempotency key; event.type — enrollment.created | access.extended | ...
res.sendStatus(200);
});<?php
$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_X_COURSTORE_SIGNATURE'] ?? ''), $sig);
$expected = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $body, getenv('COURSTORE_WEBHOOK_SECRET'));
if (abs(time() - (int)($sig['t'] ?? 0)) > 300 || !hash_equals($expected, (string)($sig['v1'] ?? ''))) {
http_response_code(401);
exit;
}
$event = json_decode($body, true);
// $event['id'] — idempotency key; $event['type'] — event type
http_response_code(200);import hashlib, hmac, os, time
from flask import Flask, request
app = Flask(__name__)
@app.post("/courstore/webhook")
def courstore_webhook():
body = request.get_data()
header = request.headers.get("X-Courstore-Signature", "")
sig = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
expected = hmac.new(os.environ["COURSTORE_WEBHOOK_SECRET"].encode(),
sig.get("t", "").encode() + b"." + body, hashlib.sha256).hexdigest()
t = sig.get("t", "")
fresh = t.isdigit() and abs(time.time() - int(t)) <= 300
if not fresh or not hmac.compare_digest(expected, sig.get("v1", "")):
return "", 401
event = request.get_json()
# event["id"] — idempotency key; event["type"] — event type
return "", 200Доставка и повторы
- Ответьте
2xxв течение 10 секунд. Тяжёлую обработку делайте после ответа, в своей очереди. - Любой другой ответ, таймаут или ошибка соединения — доставка повторится по расписанию ниже, всего 6 попыток.
- Редиректы (
3xx) не выполняются и считаются неудачей — укажите конечный адрес. - Порядок событий не гарантирован, одно событие может прийти дважды — обрабатывайте идемпотентно по
id. - События приходят с задержкой до 1–2 минут, окончание доступа (
access.expired) — до 30 минут.
| Попытка | Когда |
|---|---|
| 1 | Сразу после события |
| 2 | Через 1 минуту |
| 3 | Через 5 минут |
| 4 | Через 30 минут |
| 5 | Через 2 часа |
| 6 | Через 6 часов |
Результат каждой попытки виден в журнале доставок в карточке «API школы».
Идемпотентность
Метод POST /enrollments требует заголовок Idempotency-Key — уникальный ключ операции, например номер заказа в CRM (до 128 символов: буквы, цифры, . _ : -). Он делает повтор запроса безопасным: если ответ потерялся из-за таймаута, повторите запрос с тем же ключом, и ученик не будет зачислен дважды.
| Ситуация | Результат |
|---|---|
Тот же Idempotency-Key и то же тело | Тот же ответ, повторного действия нет |
Тот же Idempotency-Key, но другое тело | 409 idempotency_conflict |
| Запрос с этим ключом ещё выполняется | 409 idempotency_in_progress — подождите и повторите |
| Заголовок не передан | 422 idempotency_key_required |
{
"error": "idempotency_conflict",
"message": "Этот Idempotency-Key уже использован с другим телом запроса"
}Не путайте с повторной покупкой: если ученик уже зачислен на курс и вы отправляете новую операцию (другой Idempotency-Key), доступ продлевается — см. раздел «Срок доступа».
Срок доступа
Срок доступа определяется так:
- Если в запросе передан объект
access— срок берётся из него:mode=days(amountот 1 до 3650) илиmonths(amountот 1 до 120). - Если
accessне передан — срок берётся из настроек курса. Эти настройки видны вGET /coursesв полеaccess:mode=days/monthsсamount, либоunlimited(бессрочный доступ,amount=null).
{
"phone": "+77001234567",
"courseId": 1234,
"access": {
"mode": "months",
"amount": 3
}
}В ответах и в статусе зачисления: lastDay — последний день доступа, expireDate — граница закрытия доступа; null означает бессрочный доступ или ещё не известную дату.
Повторная покупка. Если ученик уже был зачислен на этот курс, доступ продлевается сразу: отсчёт идёт от большей из дат — сегодняшней или текущего конца доступа. Ответ — 200, status: done (без очереди).
Уведомления ученику
Зачисление через API идёт через ту же очередь, что и ручное зачисление из кабинета. Ученик получает те же сообщения: WhatsApp / WABA / Telegram / почта.
- Параметр
notify(по умолчаниюtrue); значениеfalseотключает уведомления для этого зачисления. - При повторной покупке (продление) ученик получает уведомление в Telegram — так же, как при «Продлить доступ» в кабинете.
- В статусе зачисления
notified: trueозначает, что уведомление поставлено в отправку. Доставку в WhatsApp/Telegram сервис не подтверждает.
{
"phone": "+77001234567",
"courseId": 1234,
"notify": false
}Лимиты
Ограничения защищают платформу и ваших учеников от случайных циклов в интеграции.
| Ограничение | Значение | При превышении |
|---|---|---|
| Запросы | 60 в минуту на ключ | 429 rate_limited и заголовок Retry-After (секунд до повтора) |
| Зачисления | 300 в сутки на школу | 429 daily_limit |
| Размер тела запроса | до 4 КБ | 422 payload_too_large |
| Активные ключи | до 3 на школу | Отзовите ненужный ключ и создайте новый |
При превышении лимита запросов ответ содержит заголовок Retry-After — число секунд до повтора. Использование за сегодня видно в карточке «API школы» в кабинете.
Ошибки
Все ошибки приходят в JSON с кодом и описанием:
{
"error": "validation_error",
"message": "phone: некорректный номер телефона"
}Ориентируйтесь на HTTP-статус и поле error (машинный код); текст message предназначен для логов.
| Код | HTTP | Что значит | Что делать |
|---|---|---|---|
bad_key | 401 | Ключ не передан, неверен или отозван | Проверьте заголовок X-School-Key; при необходимости создайте новый ключ в кабинете |
key_expired | 401 | У ключа истёк срок действия («действует до») | Выпустите новый ключ в кабинете |
tariff_not_allowed | 403 | Тариф школы не даёт доступ к API (нужен Pro годовой, Lifetime или Enterprise). Проверяется на каждом запросе | Перейдите на подходящий тариф |
course_not_found | 404 | Курс не найден в вашей школе, в том числе принадлежащий другой школе | Сверьте courseId с ответом GET /courses |
validation_error | 422 | Некорректные данные запроса (в message указано поле) | Исправьте запрос по описанию в message |
invalid_json | 422 | Тело запроса не является корректным JSON | Проверьте JSON и заголовок Content-Type |
payload_too_large | 422 | Тело запроса больше 4 КБ | Сократите тело запроса |
idempotency_key_required | 422 | Для POST /enrollments не передан Idempotency-Key | Добавьте заголовок, например с номером заказа |
idempotency_conflict | 409 | Этот Idempotency-Key уже использован с другим телом запроса | Используйте новый ключ для новой операции или отправьте исходное тело |
idempotency_in_progress | 409 | Запрос с этим ключом ещё выполняется | Подождите и повторите запрос с тем же ключом |
course_locked | 409 | Курс заморожен | Разморозьте курс в кабинете или выберите другой |
course_closed | 409 | Курс не опубликован (например, черновик) | Опубликуйте курс |
student_limit_reached | 409 | Достигнут лимит студентов по тарифу | Освободите место или перейдите на тариф с большим лимитом |
rate_limited | 429 | Больше 60 запросов в минуту на ключ | Подождите Retry-After секунд и повторите |
daily_limit | 429 | Больше 300 зачислений в сутки на школу | Повторите позже, ориентируясь на Retry-After |
Для зачисления в статусе failed поле error содержит not_confirmed: за 15 минут зачисление не подтвердилось.
Конфиденциальность
API показывает данные только вашей школы.
created: trueв ответеPOST /studentsозначает, что ученика ещё не было в вашей школе — даже если его аккаунт уже есть на платформе в другой школе. Для вашей школы создаётся связь, чужие данные не раскрываются.created: false— ученик уже учится в вашей школе. Имя и почта существующего ученика не перезаписываются.- Проверка доступа для ученика, не связанного с вашей школой, отвечает так же, как для несуществующего:
active: false. - В
GET /coursesвозвращаются только курсы школы — владельца ключа; чужой курс отвечает404 course_not_found.