Courstore API

API школы

Внешнее зачисление учеников на курсы вашей школы из CRM или другой системы. Оплата — на вашей стороне, Courstore открывает доступ и уведомляет ученика.

Базовый URL: 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=Текущий доступ ученика к курсу

Быстрый старт

  1. Проверьте тариф. API доступен на Pro (годовой), Lifetime и Enterprise.
  2. Получите ключ. В кабинете школы откройте «Интеграции» → «API школы» → «Сгенерировать ключ». Ключ вида cs_live_… показывается один раз — сразу сохраните его в надёжном месте. Можно держать до 3 активных ключей, любой можно отозвать.
  3. Отправьте первый запрос. Получите список курсов школы — в ответе будут id курсов, которые нужны для зачисления.
bash
export KEY="cs_live_..."
export BASE="https://api.courstore.com/v1/school-api"
curl
curl -s "$BASE/courses" \
  -H "X-School-Key: $KEY"
JSON · 200
[
  {
    "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
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"}'
JSON · 202
{
  "requestId": "req_882",
  "status": "queued",
  "studentId": 118350,
  "enrollmentId": null,
  "expireDate": null,
  "lastDay": null
}

Ответ 202 со статусом queued означает, что зачисление принято в очередь. Результат — в методе «Статус зачисления» (обычно готово через несколько секунд).

Аутентификация

Каждый запрос должен содержать заголовок X-School-Key с ключом школы:

curl
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:
JSON · 401
{
  "error": "bad_key",
  "message": "Неверный ключ API"
}

Если ключ верный, но тариф школы не даёт доступ к API — 403 tariff_not_allowed. В карточке «API школы» видно использование ключей за сегодня и последние ошибки.

Методы

Во всех примерах ниже используются переменные $BASE и $KEY из раздела «Быстрый старт». Пути указаны относительно базового URL https://api.courstore.com/v1/school-api.

Курсы школы

GET/courses

Возвращает курсы школы-владельца ключа. Поле access — срок доступа по умолчанию из настроек курса (используется, если при зачислении access не передан).

Параметров нет.

Пример запроса

curl
curl -s "$BASE/courses" \
  -H "X-School-Key: $KEY"

Поля ответа

ПолеТипОписание
idintegerИдентификатор курса — его передают в courseId
titlestringНазвание курса
slugstringКороткое имя курса в адресе
statusstringpublish | private | draft | pending. Зачислять можно только на опубликованный курс
access.modestringdays | months | unlimited
access.amountinteger | nullКоличество дней или месяцев; null при unlimited

Пример ответа

JSON · 200
[
  {
    "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КодЧто значит
401bad_keyКлюч не передан, неверен или отозван
401key_expiredУ ключа истёк срок действия
403tariff_not_allowedТариф школы не даёт доступ к API
429rate_limitedПревышен лимит 60 запросов в минуту

Найти или создать ученика

POST/students

Ищет ученика по телефону и возвращает его; если такого ученика в вашей школе нет — создаёт. Телефон нормализуется (+7 / 8 / пробелы / скобки). Имя и почта существующего ученика не перезаписываются. Новый ученик создаётся без пароля — вход по коду.

Тело запроса (JSON)

ПараметрТипОбязательныйОписание
phonestringдаТелефон в любом формате
namestringнетИмя, до 200 символов
emailstringнетПочта, до 200 символов
langstringнетЯзык ученика: ru | kk | en | uz. По умолчанию ru

Пример запроса

curl
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"}'

Поля ответа

ПолеТипОписание
studentIdintegerИдентификатор ученика
createdbooleantrue — ученика ещё не было в вашей школе (ответ 201); false — уже учится (ответ 200)

Пример ответа

JSON · 201
{
  "studentId": 118350,
  "created": true
}
JSON · 200
{
  "studentId": 118347,
  "created": false
}

Ошибки метода

HTTPКодЧто значит
401bad_keyКлюч не передан, неверен или отозван
401key_expiredУ ключа истёк срок действия
403tariff_not_allowedТариф школы не даёт доступ к API
409student_limit_reachedДостигнут лимит студентов по тарифу
422validation_errorНекорректные данные, например номер телефона; также invalid_json и payload_too_large
429rate_limitedПревышен лимит 60 запросов в минуту

Зачислить на курс

POST/enrollments

Зачисляет ученика на курс. Если ученика с таким телефоном нет — он создаётся. Срок доступа берётся из access, если он передан, иначе из настроек курса. Заголовок Idempotency-Key обязателен.

Заголовок и тело запроса

ПараметрТипОбязательныйОписание
Idempotency-Keyheader, stringдаУникальный ключ операции (например, номер заказа в CRM), до 128 символов: буквы, цифры, . _ : -
phonestringдаТелефон ученика в любом формате
courseIdintegerдаИдентификатор курса из GET /courses
namestringнетИмя, до 200 символов (используется для нового ученика)
emailstringнетПочта, до 200 символов
accessobjectнетСрок доступа. Если не передан — из настроек курса
access.modestringнетdays | months
access.amountintegerнетdays: 1…3650; months: 1…120
notifybooleanнетОтправлять ученику уведомления о зачислении. По умолчанию true
orderRefstringнетВаш номер заказа, до 128 символов (для вашего учёта)

Пример запроса

curl
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"}'

Поля ответа

ПолеТипОписание
requestIdstringИдентификатор запроса — для GET /enrollments/{requestId}
statusstringqueued — принято в очередь; done — выполнено
studentIdinteger | nullИдентификатор ученика
enrollmentIdinteger | nullИдентификатор зачисления; null, пока запрос в очереди
expireDatedate | nullГраница закрытия доступа; null — бессрочно или ещё не известна
lastDaydate | nullПоследний день доступа

Пример ответа

JSON · 202
{
  "requestId": "req_882",
  "status": "queued",
  "studentId": 118350,
  "enrollmentId": null,
  "expireDate": null,
  "lastDay": null
}
JSON · 200
{
  "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КодЧто значит
401bad_keyКлюч не передан, неверен или отозван
401key_expiredУ ключа истёк срок действия
403tariff_not_allowedТариф школы не даёт доступ к API
404course_not_foundКурс не найден в вашей школе (в том числе чужой курс)
409idempotency_conflictЭтот Idempotency-Key уже использован с другим телом
409idempotency_in_progressЗапрос с этим ключом ещё выполняется
409course_lockedКурс заморожен
409course_closedКурс не опубликован (например, черновик)
409student_limit_reachedДостигнут лимит студентов по тарифу
422idempotency_key_requiredНе передан заголовок Idempotency-Key; также validation_error, invalid_json, payload_too_large
429rate_limitedПревышен лимит 60 запросов в минуту
429daily_limitПревышен лимит 300 зачислений в сутки на школу

Статус зачисления

GET/enrollments/{requestId}

Возвращает состояние зачисления, принятого методом POST /enrollments.

Параметры пути

ПараметрТипОбязательныйОписание
requestIdstringдаИдентификатор из ответа на зачисление, например req_882

Пример запроса

curl
curl -s "$BASE/enrollments/req_882" \
  -H "X-School-Key: $KEY"

Поля ответа

ПолеТипОписание
requestIdstringИдентификатор запроса
statusstringqueued — ждёт обработки; done — ученик зачислен; failed — за 15 минут зачисление не подтвердилось
studentIdinteger | nullИдентификатор ученика
courseIdinteger | nullИдентификатор курса
expireDatedate | nullГраница закрытия доступа
lastDaydate | nullПоследний день доступа
notifiedbooleantrue — уведомление ученику поставлено в отправку (доставку сервис не подтверждает)
errorstring | nullПричина при failed: not_confirmed

Пример ответа

JSON · 200
{
  "requestId": "req_882",
  "status": "done",
  "studentId": 118350,
  "courseId": 1234,
  "expireDate": "2027-01-20",
  "lastDay": "2027-01-19",
  "notified": true,
  "error": null
}

Ошибки метода

HTTPКодЧто значит
401bad_keyКлюч не передан, неверен или отозван
401key_expiredУ ключа истёк срок действия
403tariff_not_allowedТариф школы не даёт доступ к API
404—Запрос с таким requestId не найден
429rate_limitedПревышен лимит 60 запросов в минуту

Проверка доступа

GET/enrollments?phone=&courseId=

Показывает текущий доступ ученика к курсу вашей школы. Ученик, не связанный с вашей школой, отвечает так же, как несуществующий: active: false.

Параметры запроса (query)

ПараметрТипОбязательныйОписание
phonestringдаТелефон ученика. Знак + в URL передавайте как %2B
courseIdintegerдаИдентификатор курса

Пример запроса

curl
curl -s "$BASE/enrollments?phone=%2B77001234567&courseId=1234" \
  -H "X-School-Key: $KEY"

Поля ответа

ПолеТипОписание
activebooleanЕсть ли у ученика действующий доступ
expireDatedate | nullГраница закрытия доступа
lastDaydate | nullПоследний день доступа
progressintegerПрогресс по курсу, %

Пример ответа

JSON · 200
{
  "active": true,
  "expireDate": "2026-12-01",
  "lastDay": "2026-11-30",
  "progress": 35
}

Ошибки метода

HTTPКодЧто значит
401bad_keyКлюч не передан, неверен или отозван
401key_expiredУ ключа истёк срок действия
403tariff_not_allowedТариф школы не даёт доступ к API
404course_not_foundКурс не найден в вашей школе
422validation_errorНе переданы или некорректны phone / courseId
429rate_limitedПревышен лимит 60 запросов в минуту

Ссылка входа для ученика

Ваш сервис может выдать ученику одноразовую ссылку, которая откроет кабинет вашей школы на Courstore без пароля. Токен в ссылке — 32 случайных байта, у нас хранится только хеш, после первого использования ссылка гаснет.

Ссылка входа в Courstore

Ученик уже вошёл в ваш сервис — дайте ему ссылку, которая откроет кабинет вашей школы на Courstore без пароля. Ссылка одноразовая.

POST/students/login-link

Тело запроса

ПараметрТипОбязательныйОписание
phonestringдаТелефон ученика в любом формате
redirectintegerнетid курса вашей школы: после входа откроется страница курса. Без него — «Мои курсы»
ttlSecondsintegerнетСрок жизни ссылки, от 30 до 86400 секунд, по умолчанию 120

Пример запроса

curl
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}'

Пример ответа

JSON · 200
{
  "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
curl -s -X POST "$BASE/students/login-link" \
  -H "X-School-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+77001234567","ttlSeconds":86400}'

Если ссылка устарела или её уже открыли, ученик не увидит ошибку: он попадёт на обычную страницу входа вашей школы с подсказкой «Ссылка устарела — войдите по номеру телефона».

Пример на сервере

Node.js (Express)
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КодЧто значит
404student_not_foundНомер не принадлежит ученику вашей школы. Автору, сотрудникам и администраторам ссылки входа не выдаются никогда — для них тоже этот ответ
404course_not_foundКурс из redirect не принадлежит вашей школе
422validation_errorНекорректный phone, redirect или ttlSeconds вне 30…86400
429daily_limitБольше 2000 ссылок в сутки на школу; см. заголовок Retry-After
429rate_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 — бессрочно.

Тело запроса

JSON · enrollment.created
{
  "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"
  }
}
JSON · access.revoked
{
  "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": "Основы маркетинга"
    }
  }
}

Поля ответа

ПолеТипОписание
idstringУникальный id события evt_…; совпадает с X-Courstore-Delivery. Ключ идемпотентности
typestringТип события
createdAtdate-timeКогда произошло событие (UTC)
schoolIdintegerid вашей школы на Courstore
data.enrollmentIdintegerid зачисления
data.studentobjectid, name, phone, email ученика
data.courseobjectid и title курса
data.accessUntildate | nullПоследний день доступа; null — бессрочно. В access.revoked поля нет

Заголовки и подпись

ЗаголовокОписание
X-Courstore-EventТип события
X-Courstore-Deliveryid события (то же, что id в теле) — для защиты от повторов
X-Courstore-Signaturet=<unix-время>,v1=<hex>, где v1 = HMAC-SHA256(секрет, t + "." + сырое тело)
HTTP
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…
  1. Возьмите сырое тело запроса — до разбора JSON: любое переформатирование ломает подпись.
  2. Посчитайте HMAC-SHA256 секретом whsec_… от строки t + "." + тело, в hex.
  3. Сравните с v1 сравнением за постоянное время (timingSafeEqual, hash_equals, compare_digest).
  4. Отклоняйте запросы, у которых t старше 5 минут, — это защита от повторной отправки перехваченного запроса.

Пример проверки подписи

Node.js (Express)
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
<?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);
Python (Flask)
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
JSON · 409
{
  "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).
JSON
{
  "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 сервис не подтверждает.
JSON
{
  "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 с кодом и описанием:

JSON · 422
{
  "error": "validation_error",
  "message": "phone: некорректный номер телефона"
}

Ориентируйтесь на HTTP-статус и поле error (машинный код); текст message предназначен для логов.

КодHTTPЧто значитЧто делать
bad_key401Ключ не передан, неверен или отозванПроверьте заголовок X-School-Key; при необходимости создайте новый ключ в кабинете
key_expired401У ключа истёк срок действия («действует до»)Выпустите новый ключ в кабинете
tariff_not_allowed403Тариф школы не даёт доступ к API (нужен Pro годовой, Lifetime или Enterprise). Проверяется на каждом запросеПерейдите на подходящий тариф
course_not_found404Курс не найден в вашей школе, в том числе принадлежащий другой школеСверьте courseId с ответом GET /courses
validation_error422Некорректные данные запроса (в message указано поле)Исправьте запрос по описанию в message
invalid_json422Тело запроса не является корректным JSONПроверьте JSON и заголовок Content-Type
payload_too_large422Тело запроса больше 4 КБСократите тело запроса
idempotency_key_required422Для POST /enrollments не передан Idempotency-KeyДобавьте заголовок, например с номером заказа
idempotency_conflict409Этот Idempotency-Key уже использован с другим телом запросаИспользуйте новый ключ для новой операции или отправьте исходное тело
idempotency_in_progress409Запрос с этим ключом ещё выполняетсяПодождите и повторите запрос с тем же ключом
course_locked409Курс замороженРазморозьте курс в кабинете или выберите другой
course_closed409Курс не опубликован (например, черновик)Опубликуйте курс
student_limit_reached409Достигнут лимит студентов по тарифуОсвободите место или перейдите на тариф с большим лимитом
rate_limited429Больше 60 запросов в минуту на ключПодождите Retry-After секунд и повторите
daily_limit429Больше 300 зачислений в сутки на школуПовторите позже, ориентируясь на Retry-After

Для зачисления в статусе failed поле error содержит not_confirmed: за 15 минут зачисление не подтвердилось.

Конфиденциальность

API показывает данные только вашей школы.

  • created: true в ответе POST /students означает, что ученика ещё не было в вашей школе — даже если его аккаунт уже есть на платформе в другой школе. Для вашей школы создаётся связь, чужие данные не раскрываются.
  • created: false — ученик уже учится в вашей школе. Имя и почта существующего ученика не перезаписываются.
  • Проверка доступа для ученика, не связанного с вашей школой, отвечает так же, как для несуществующего: active: false.
  • В GET /courses возвращаются только курсы школы — владельца ключа; чужой курс отвечает 404 course_not_found.