Courstore API

Мектеп API

Мектебіңіздің курстарына студенттерді CRM-нен немесе басқа жүйеден қабылдаңыз. Төлем сіздің жағыңызда қалады, ал Courstore қолжетімділікті ашып, студентке хабарлайды.

Негізгі URL: https://api.courstore.com/v1/school-apiSwagger спецификациясы →

Шолу

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
}

queued мәртебесі бар 202 жауабы қабылдау кезекке қабылданғанын білдіреді. Нәтижені «Қабылдау мәртебесі» әдісінен алыңыз (әдетте бірнеше секундтан кейін дайын болады).

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

Әр сұраныста мектеп кілті бар 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Күн немесе ай саны; unlimited кезінде null

Жауап мысалы

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_requiredIdempotency-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 | nullfailed кезіндегі себеп: 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_errorphone / 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_foundredirect ішіндегі курс мектебіңізге тиесілі емес
422validation_errorphone, 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)
schoolIdintegerCourstore-дағы мектебіңіздің id-і
data.enrollmentIdintegerЖазылу id-і
data.studentobjectОқушының id, name, phone, email
data.courseobjectКурстың id және title
data.accessUntildate | nullҚолжетімділіктің соңғы күні; null — мерзімсіз. access.revoked ішінде өріс жоқ

Тақырыптар мен қолтаңба

ТақырыпСипаттама
X-Courstore-EventОқиға түрі
X-Courstore-DeliveryОқиға id-і (денедегі 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. whsec_… құпиясымен t + "." + дене жолынан HMAC-SHA256 есептеңіз, 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

Жеткізу және қайталау

  • 10 секунд ішінде 2xx деп жауап беріңіз. Ауыр өңдеуді жауаптан кейін өз кезегіңізде жасаңыз.
  • Кез келген басқа жауап, күту уақытының өтуі немесе қосылу қатесі — жеткізу төмендегі кесте бойынша қайталанады, барлығы 6 әрекет.
  • Қайта бағыттаулар (3xx) орындалмайды және сәтсіздік болып саналады — соңғы мекенжайды көрсетіңіз.
  • Оқиғалар реті кепілдендірілмейді, бір оқиға екі рет келуі мүмкін — id бойынша идемпотентті өңдеңіз.
  • Оқиғалар 1–2 минутқа дейін кешігіп келеді, қолжетімділіктің аяқталуы (access.expired) — 30 минутқа дейін.
ӘрекетҚашан
1Оқиғадан кейін бірден
21 минуттан кейін
35 минуттан кейін
430 минуттан кейін
52 сағаттан кейін
66 сағаттан кейін

Әр әрекеттің нәтижесі «Мектеп 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
}

Лимиттер

Шектеулер платформаны және студенттеріңізді интеграциядағы кездейсоқ циклдерден қорғайды.

ШектеуМәніАсып кетсе
Сұраныстаркілтке минутына 60429 rate_limited және Retry-After тақырыбы (қайталауға дейінгі секунд)
Қабылдаулармектепке тәулігіне 300429 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_required422POST /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 тек сіздің мектебіңіздің деректерін көрсетеді.

  • POST /students жауабындағы created: true студент сіздің мектебіңізде әлі болмағанын білдіреді — оның аккаунты платформада басқа мектепте бар болса да. Сіздің мектебіңіз үшін байланыс жасалады, бөтен деректер ашылмайды.
  • created: false — студент сіздің мектебіңізде бұрыннан оқиды. Бар студенттің аты мен поштасы қайта жазылмайды.
  • Мектебіңізбен байланысы жоқ студент үшін қолжетімділікті тексеру жоқ студентпен бірдей жауап береді: active: false.
  • GET /courses тек кілт иесі мектебінің курстарын қайтарады; бөтен курс 404 course_not_found жауабын береді.