Courstore API

Maktab API

Maktabingiz kurslariga talabalarni CRM yoki boshqa tizimdan qabul qiling. To‘lov sizning tomoningizda qoladi, Courstore esa kirishni ochib, talabaga xabar beradi.

Asosiy URL: https://api.courstore.com/v1/school-apiSwagger spetsifikatsiyasi →

Umumiy ko‘rinish

School API tizimingizga (CRM, to‘lov xizmati, o‘z saytingiz) talabalarni Courstore’dagi maktab kurslariga qabul qilish va ularning kirishini tekshirish imkonini beradi. To‘lovni o‘zingiz qabul qilasiz — API faqat kursga kirishni ochadi.

  • Asosiy URL: https://api.courstore.com/v1/school-api
  • Format: JSON. So‘rov tanasi — Content-Type: application/json, hajmi 4 KB gacha.
  • Avtorizatsiya: X-School-Key sarlavhasidagi maktab kaliti.
  • Kirish: Pro (yillik), Lifetime va Enterprise tariflari. Boshqa tariflarda API 403 tariff_not_allowed qaytaradi — tarif har bir so‘rovda tekshiriladi.
  • Xatolar doim JSON ko‘rinishida keladi: {"error": "kod", "message": "matn"}.

To‘liq mashina o‘qiydigan spetsifikatsiya: Swagger.

MetodURLVazifasi
GET/coursesMaktab kurslari ro‘yxati va odatiy kirish muddati
POST/studentsTalabani telefon bo‘yicha topish yoki yaratish
POST/enrollmentsTalabani kursga qabul qilish (talaba bo‘lmasa, yaratadi)
GET/enrollments/{requestId}requestId bo‘yicha qabul holati
GET/enrollments?phone=&courseId=Talabaning kursga joriy kirishi

Tez boshlash

  1. Tarifni tekshiring. API Pro (yillik), Lifetime va Enterprise tariflarida mavjud.
  2. Kalit oling. Maktab kabinetida «Integratsiyalar» → «Maktab API» → «Kalit yaratish» bo‘limini oching. cs_live_… ko‘rinishidagi kalit faqat bir marta ko‘rsatiladi — darhol ishonchli joyga saqlab qo‘ying. 3 tagacha faol kalit saqlash mumkin, istalganini bekor qilish mumkin.
  3. Birinchi so‘rovni yuboring. Maktab kurslari ro‘yxatini oling — javobda qabul qilish uchun kerak bo‘ladigan kurslarning id qiymatlari bo‘ladi.
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
    }
  }
]

Keyin talabani kursga qabul qiling — batafsil «Kursga qabul qilish» metodida:

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 holatidagi 202 javobi qabul navbatga olinganini bildiradi. Natijani «Qabul holati» metodidan oling (odatda bir necha soniyada tayyor bo‘ladi).

Autentifikatsiya

Har bir so‘rovda maktab kaliti bilan X-School-Key sarlavhasi bo‘lishi kerak:

curl
curl -s "https://api.courstore.com/v1/school-api/courses" \
  -H "X-School-Key: cs_live_..."
  • Kalit kabinetda yaratiladi: «Integratsiyalar» → «Maktab API». Formati — cs_live_…, bir marta ko‘rsatiladi.
  • Maktabga 3 tagacha faol kalit beriladi. Istalgan kalitni shu kartochkada bekor qilish mumkin; uni ishlatadigan integratsiyalar ishlashdan to‘xtaydi.
  • Kalitni o‘z serveringizda saqlang. Uni mobil ilovalarga, brauzer kodiga va ochiq repozitoriylarga joylashtirmang.
  • Muddat majburiy emas: kalit yaratishda «gacha amal qiladi» sanasini ko‘rsatish mumkin (shu kun ham kiradi). Undan keyin kalit 401 key_expired qaytaradi — yangisini yarating.
  • Maktab tarifi tugasa, uning barcha kalitlari avtomatik va butunlay bekor qilinadi; tarifni uzaytirgach yangi kalit yarating. Bekor qilingan kalit 401 bad_key oladi.
  • Kalit berilmasa, noto‘g‘ri bo‘lsa yoki bekor qilingan bo‘lsa — 401 javobi keladi:
JSON · 401
{
  "error": "bad_key",
  "message": "Неверный ключ API"
}

Kalit to‘g‘ri, lekin maktab tarifi API’ga kirishni bermasa — 403 tariff_not_allowed. «Maktab API» kartochkasida bugungi kalit ishlatilishi va oxirgi xatolar ko‘rinadi.

Metodlar

Quyidagi misollarda «Tez boshlash» bo‘limidagi $BASE va $KEY o‘zgaruvchilari ishlatiladi. Yo‘llar asosiy URL — https://api.courstore.com/v1/school-api — ga nisbatan ko‘rsatilgan.

Maktab kurslari

GET/courses

Kalit egasi maktabning kurslarini qaytaradi. access maydoni — kurs sozlamalaridagi odatiy kirish muddati (qabul qilishda access berilmasa ishlatiladi).

Parametrlar yo‘q.

So‘rov misoli

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

Javob maydonlari

MaydonTuriTavsif
idintegerKurs identifikatori — uni courseId sifatida beriladi
titlestringKurs nomi
slugstringManzildagi kursning qisqa nomi
statusstringpublish | private | draft | pending. Faqat e’lon qilingan kursga qabul qilish mumkin
access.modestringdays | months | unlimited
access.amountinteger | nullKunlar yoki oylar soni; unlimited bo‘lsa null

Javob misoli

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
    }
  }
]

Metod xatolari

HTTPKodMa’nosi
401bad_keyKalit berilmagan, noto‘g‘ri yoki bekor qilingan
401key_expiredKalitning amal qilish muddati tugagan
403tariff_not_allowedMaktab tarifi API’ga kirishni bermaydi
429rate_limitedDaqiqasiga 60 so‘rov limiti oshib ketdi

Talabani topish yoki yaratish

POST/students

Talabani telefon bo‘yicha qidirib, qaytaradi; sizning maktabingizda bunday talaba bo‘lmasa — yaratadi. Telefon me’yorlashtiriladi (+7 / 8 / bo‘shliqlar / qavslar). Mavjud talabaning ismi va pochtasi qayta yozilmaydi. Yangi talaba parolsiz yaratiladi — kirish kod orqali.

So‘rov tanasi (JSON)

ParametrTuriMajburiyTavsif
phonestringhaIstalgan formatdagi telefon
namestringyo‘qIsm, 200 belgigacha
emailstringyo‘qPochta, 200 belgigacha
langstringyo‘qTalaba tili: ru | kk | en | uz. Odatiy ru

So‘rov misoli

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

Javob maydonlari

MaydonTuriTavsif
studentIdintegerTalaba identifikatori
createdbooleantrue — talaba sizning maktabingizda hali bo‘lmagan (javob 201); false — allaqachon o‘qiydi (javob 200)

Javob misoli

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

Metod xatolari

HTTPKodMa’nosi
401bad_keyKalit berilmagan, noto‘g‘ri yoki bekor qilingan
401key_expiredKalitning amal qilish muddati tugagan
403tariff_not_allowedMaktab tarifi API’ga kirishni bermaydi
409student_limit_reachedTarif bo‘yicha talabalar limitiga yetildi
422validation_errorMa’lumotlar noto‘g‘ri, masalan telefon raqami; shuningdek invalid_json va payload_too_large
429rate_limitedDaqiqasiga 60 so‘rov limiti oshib ketdi

Kursga qabul qilish

POST/enrollments

Talabani kursga qabul qiladi. Bunday telefonli talaba bo‘lmasa, u yaratiladi. Kirish muddati access berilsa undan, aks holda kurs sozlamalaridan olinadi. Idempotency-Key sarlavhasi majburiy.

Sarlavha va so‘rov tanasi

ParametrTuriMajburiyTavsif
Idempotency-Keyheader, stringhaOperatsiyaning noyob kaliti (masalan, CRM’dagi buyurtma raqami), 128 belgigacha: harflar, raqamlar, . _ : -
phonestringhaTalabaning istalgan formatdagi telefoni
courseIdintegerhaGET /courses javobidagi kurs identifikatori
namestringyo‘qIsm, 200 belgigacha (yangi talaba uchun ishlatiladi)
emailstringyo‘qPochta, 200 belgigacha
accessobjectyo‘qKirish muddati. Berilmasa — kurs sozlamalaridan
access.modestringyo‘qdays | months
access.amountintegeryo‘qdays: 1…3650; months: 1…120
notifybooleanyo‘qTalabaga qabul haqida xabarnomalar yuborish. Odatiy true
orderRefstringyo‘qSizning buyurtma raqamingiz, 128 belgigacha (o‘z hisobingiz uchun)

So‘rov misoli

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

Javob maydonlari

MaydonTuriTavsif
requestIdstringSo‘rov identifikatori — GET /enrollments/{requestId} uchun
statusstringqueued — navbatga olindi; done — bajarildi
studentIdinteger | nullTalaba identifikatori
enrollmentIdinteger | nullQabul identifikatori; so‘rov navbatda turganda null
expireDatedate | nullKirish yopiladigan chegara; null — muddatsiz yoki hali noma’lum
lastDaydate | nullKirishning oxirgi kuni

Javob misoli

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

Yangi qabul — javob 202, status: queued. Qabul va xabarnomalar taxminan bir daqiqa ichida bajariladi (amalda — soniyalar); natija — GET /enrollments/{requestId} metodida.

Qayta xarid (talaba kursga allaqachon qabul qilingan) — kirish darhol uzaytiriladi, javob 200, status: done.

Metod xatolari

HTTPKodMa’nosi
401bad_keyKalit berilmagan, noto‘g‘ri yoki bekor qilingan
401key_expiredKalitning amal qilish muddati tugagan
403tariff_not_allowedMaktab tarifi API’ga kirishni bermaydi
404course_not_foundKurs sizning maktabingizda topilmadi (begona kursni ham qo‘shgan holda)
409idempotency_conflictBu Idempotency-Key boshqa tana bilan ishlatilgan
409idempotency_in_progressShu kalitli so‘rov hali bajarilmoqda
409course_lockedKurs muzlatilgan
409course_closedKurs e’lon qilinmagan (masalan, qoralama)
409student_limit_reachedTarif bo‘yicha talabalar limitiga yetildi
422idempotency_key_requiredIdempotency-Key sarlavhasi berilmagan; shuningdek validation_error, invalid_json, payload_too_large
429rate_limitedDaqiqasiga 60 so‘rov limiti oshib ketdi
429daily_limitMaktab uchun sutkasiga 300 ta qabul limiti oshib ketdi

Qabul holati

GET/enrollments/{requestId}

POST /enrollments metodi qabul qilgan qabulning holatini qaytaradi.

Yo‘l parametrlari

ParametrTuriMajburiyTavsif
requestIdstringhaQabul javobidagi identifikator, masalan req_882

So‘rov misoli

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

Javob maydonlari

MaydonTuriTavsif
requestIdstringSo‘rov identifikatori
statusstringqueued — qayta ishlashni kutmoqda; done — talaba qabul qilindi; failed — 15 daqiqa ichida qabul tasdiqlanmadi
studentIdinteger | nullTalaba identifikatori
courseIdinteger | nullKurs identifikatori
expireDatedate | nullKirish yopiladigan chegara
lastDaydate | nullKirishning oxirgi kuni
notifiedbooleantrue — talabaga xabarnoma yuborishga qo‘yildi (yetkazilishini xizmat tasdiqlamaydi)
errorstring | nullfailed bo‘lganda sabab: not_confirmed

Javob misoli

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

Metod xatolari

HTTPKodMa’nosi
401bad_keyKalit berilmagan, noto‘g‘ri yoki bekor qilingan
401key_expiredKalitning amal qilish muddati tugagan
403tariff_not_allowedMaktab tarifi API’ga kirishni bermaydi
404—Bunday requestId li so‘rov topilmadi
429rate_limitedDaqiqasiga 60 so‘rov limiti oshib ketdi

Kirishni tekshirish

GET/enrollments?phone=&courseId=

Talabaning maktabingiz kursiga joriy kirishini ko‘rsatadi. Maktabingiz bilan bog‘lanmagan talaba mavjud bo‘lmagan talaba bilan bir xil javob beradi: active: false.

So‘rov parametrlari (query)

ParametrTuriMajburiyTavsif
phonestringhaTalaba telefoni. URL’dagi + belgisini %2B ko‘rinishida bering
courseIdintegerhaKurs identifikatori

So‘rov misoli

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

Javob maydonlari

MaydonTuriTavsif
activebooleanTalabada amaldagi kirish bormi
expireDatedate | nullKirish yopiladigan chegara
lastDaydate | nullKirishning oxirgi kuni
progressintegerKurs bo‘yicha progress, %

Javob misoli

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

Metod xatolari

HTTPKodMa’nosi
401bad_keyKalit berilmagan, noto‘g‘ri yoki bekor qilingan
401key_expiredKalitning amal qilish muddati tugagan
403tariff_not_allowedMaktab tarifi API’ga kirishni bermaydi
404course_not_foundKurs sizning maktabingizda topilmadi
422validation_errorphone / courseId berilmagan yoki noto‘g‘ri
429rate_limitedDaqiqasiga 60 so‘rov limiti oshib ketdi

O‘quvchi uchun kirish havolasi

Servisingiz o‘quvchiga bir martalik havola bera oladi: u parolsiz Courstore’dagi maktabingiz kabinetini ochadi. Havoladagi token — 32 tasodifiy bayt, bizda faqat xesh saqlanadi, birinchi ishlatilgandan keyin havola o‘chadi.

Courstore’ga kirish havolasi

O‘quvchi servisingizga kirgan — unga Courstore’dagi maktabingiz kabinetini parolsiz ochadigan havola bering. Havola bir martalik.

POST/students/login-link

So‘rov tanasi

ParametrTuriMajburiyTavsif
phonestringhaO‘quvchi telefoni istalgan formatda
redirectintegeryo‘qMaktabingiz kursi id’si: kirgandan keyin kurs sahifasi ochiladi. Usiz — «Mening kurslarim»
ttlSecondsintegeryo‘qHavola amal qilish muddati, 30 dan 86400 soniyagacha, standart 120

So‘rov misoli

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

Javob misoli

JSON · 200
{
  "url": "https://myschool.courstore.com/ru/login-via-token?token=csl_...&returnUrl=courses%2Fosnovy-marketinga",
  "expiresAt": "2026-10-16T10:02:00Z"
}

expiresAt — havola qaysi vaqtgacha (UTC) amal qiladi.

Qaysi muddatni tanlash kerak

SsenariyttlSecondsNega
Saytingizdagi «Courstore’ga o‘tish» tugmasi: havolani olib, darhol yo‘naltirish120 (standart)Havola bir necha soniya kerak; qisqa muddat — manzil tarqalib ketsa, xavf kam
Xat, WhatsApp yoki Telegram’dagi havola86400 gacha (24 soat)O‘quvchi xabarni keyinroq ochishi mumkin. Havola baribir faqat bir marta ishlaydi
curl
curl -s -X POST "$BASE/students/login-link" \
  -H "X-School-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"phone":"+77001234567","ttlSeconds":86400}'

Havola eskirgan yoki allaqachon ochilgan bo‘lsa, o‘quvchi xato ko‘rmaydi: u maktabingizning oddiy kirish sahifasiga «Havola eskirgan — telefon raqamingiz orqali kiring» degan maslahat bilan tushadi.

Serverdagi misol

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);
});

Metod xatolari

HTTPKodMa’nosi
404student_not_foundRaqam maktabingiz o‘quvchisiga tegishli emas. Muallif, xodimlar va administratorlarga kirish havolasi hech qachon berilmaydi — ularga ham shu javob
404course_not_foundredirect dagi kurs maktabingizga tegishli emas
422validation_errorphone, redirect noto‘g‘ri yoki ttlSeconds 30…86400 oralig‘idan tashqarida
429daily_limitMaktabga sutkasiga 2000 tadan ortiq havola; Retry-After sarlavhasiga qarang
429rate_limitedKalitga daqiqasiga 60 tadan ortiq so‘rov (API umumiy limiti)

Xavfsizlik. Havola o‘quvchi akkauntiga kirish bilan teng: uni loglarga yozmang, uchinchi shaxslarga yubormang va faqat servisingizga kirgan o‘quvchiga bering.

Vebxuklar

Vebxuklar tizimingizga qabul qilishlar va kirish o‘zgarishlari haqida API’ni so‘ramasdan darhol xabar beradi: Courstore manzilingizga o‘zi POST yuboradi.

Sozlash

  • Kabinetda: «Integratsiyalar» → «Maktab API» → «Vebxuklar» → URL. Faqat https:// va faqat ommaviy manzil: ichki va mahalliy manzillar hamda IP-manzillar taqiqlangan.
  • Birinchi saqlashda whsec_… imzo siri beriladi. U bir marta ko‘rsatiladi — darhol saqlab qo‘ying. Sirni qayta chiqarish mumkin: eskisi darhol ishlamay qoladi.
  • «Test yuborish» tugmasi webhook.test hodisasini yuboradi. Jurnalda oxirgi 20 ta yetkazish ko‘rinadi: hodisa, javob kodi, urinish, vaqt.

Hodisalar

Hodisa turi type maydonida va X-Courstore-Event sarlavhasida keladi.

typeQachon keladiaccessUntil
enrollment.createdO‘quvchi kursga qabul qilindi: to‘lov, kabinetda qo‘lda qabul qilish, School APIKirishning oxirgi kuni yoki null
access.extendedKirish uzaytirildi yoki kurs qayta sotib olindiYangi oxirgi kun
access.expiredKirish muddati tugadi. Faqat vebxuk yoqilgandan keyingi tugashlar uchun keladiKirishning oxirgi kuni
access.revokedMuallif o‘quvchining kursga kirishini yopdiMaydon yo‘q
webhook.testKabinetdagi «Test yuborish» tugmasi—

accessUntil — kirishning oxirgi kuni, shu kun ham (YYYY-MM-DD), null — muddatsiz.

So‘rov tanasi

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": "Основы маркетинга"
    }
  }
}

Javob maydonlari

MaydonTuriTavsif
idstringHodisaning noyob id’si evt_…; X-Courstore-Delivery bilan bir xil. Idempotentlik kaliti
typestringHodisa turi
createdAtdate-timeHodisa sodir bo‘lgan vaqt (UTC)
schoolIdintegerCourstore’dagi maktabingiz id’si
data.enrollmentIdintegerQabul qilish id’si
data.studentobjectO‘quvchining id, name, phone, email
data.courseobjectKursning id va title
data.accessUntildate | nullKirishning oxirgi kuni; null — muddatsiz. access.revoked da maydon yo‘q

Sarlavhalar va imzo

SarlavhaTavsif
X-Courstore-EventHodisa turi
X-Courstore-DeliveryHodisa id’si (tanadagi id bilan bir xil) — takrorlardan himoya uchun
X-Courstore-Signaturet=<unix-vaqt>,v1=<hex>, bunda v1 = HMAC-SHA256(sir, t + "." + xom tana)
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. So‘rovning xom tanasini oling — JSON tahlilidan oldin: har qanday qayta formatlash imzoni buzadi.
  2. whsec_… siri bilan t + "." + tana satridan HMAC-SHA256 ni hex ko‘rinishida hisoblang.
  3. Uni v1 bilan doimiy vaqtli taqqoslash orqali solishtiring (timingSafeEqual, hash_equals, compare_digest).
  4. t 5 daqiqadan eski bo‘lgan so‘rovlarni rad eting — bu ushlab qolingan so‘rovni qayta yuborishdan himoya qiladi.

Imzoni tekshirish misoli

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

Yetkazish va takrorlash

  • 10 soniya ichida 2xx bilan javob bering. Og‘ir ishlovni javobdan keyin o‘z navbatingizda bajaring.
  • Boshqa har qanday javob, vaqt tugashi yoki ulanish xatosi — yetkazish quyidagi jadval bo‘yicha takrorlanadi, jami 6 urinish.
  • Qayta yo‘naltirishlar (3xx) bajarilmaydi va muvaffaqiyatsizlik hisoblanadi — yakuniy manzilni kiriting.
  • Hodisalar tartibi kafolatlanmaydi, bitta hodisa ikki marta kelishi mumkin — id bo‘yicha idempotent ishlang.
  • Hodisalar 1–2 daqiqagacha kechikib keladi, kirish tugashi (access.expired) — 30 daqiqagacha.
UrinishQachon
1Hodisadan keyin darhol
21 daqiqadan keyin
35 daqiqadan keyin
430 daqiqadan keyin
52 soatdan keyin
66 soatdan keyin

Har bir urinish natijasi «Maktab API» kartasidagi yetkazish jurnalida ko‘rinadi.

Idempotentlik

POST /enrollments metodi Idempotency-Key sarlavhasini talab qiladi — operatsiyaning noyob kaliti, masalan CRM’dagi buyurtma raqami (128 belgigacha: harflar, raqamlar, . _ : -). U so‘rovni takrorlashni xavfsiz qiladi: taym-aut sababli javob yo‘qolsa, so‘rovni shu kalit bilan takrorlang — talaba ikki marta qabul qilinmaydi.

VaziyatNatija
Bir xil Idempotency-Key va bir xil tanaBir xil javob, takroriy harakat yo‘q
Bir xil Idempotency-Key, lekin boshqa tana409 idempotency_conflict
Shu kalitli so‘rov hali bajarilmoqda409 idempotency_in_progress — kutib, takrorlang
Sarlavha berilmagan422 idempotency_key_required
JSON · 409
{
  "error": "idempotency_conflict",
  "message": "Этот Idempotency-Key уже использован с другим телом запроса"
}

Buni qayta xarid bilan adashtirmang: talaba kursga allaqachon qabul qilingan bo‘lsa va siz yangi operatsiya yuborsangiz (boshqa Idempotency-Key), kirish muddati uzaytiriladi — «Kirish muddati» bo‘limiga qarang.

Kirish muddati

Kirish muddati quyidagicha aniqlanadi:

  • So‘rovda access obyekti berilsa, muddat undan olinadi: mode = days (amount 1 dan 3650 gacha) yoki months (amount 1 dan 120 gacha).
  • access berilmasa, muddat kurs sozlamalaridan olinadi. Ularni GET /courses javobidagi access maydonida ko‘rish mumkin: mode = days / months va amount, yoki unlimited (muddatsiz kirish, amount = null).
JSON
{
  "phone": "+77001234567",
  "courseId": 1234,
  "access": {
    "mode": "months",
    "amount": 3
  }
}

Javoblarda va qabul holatida: lastDay — kirishning oxirgi kuni, expireDate — kirish yopiladigan chegara; null — muddatsiz kirish yoki sana hali noma’lum.

Qayta xarid. Talaba bu kursga avval qabul qilingan bo‘lsa, kirish darhol uzaytiriladi: hisob ikki sananing kattasidan — bugundan yoki kirishning joriy tugashidan — boshlanadi. Javob — 200, status: done (navbatsiz).

Talabaga xabarnomalar

API orqali qabul kabinetdan qo‘lda qabul qilish bilan bir xil navbat orqali o‘tadi. Talaba bir xil xabarlarni oladi: WhatsApp / WABA / Telegram / pochta.

  • notify parametri (odatiy true); false qiymati shu qabul uchun xabarnomalarni o‘chiradi.
  • Qayta xaridda (uzaytirish) talaba Telegram’da xabarnoma oladi — kabinetdagi «Kirishni uzaytirish» kabi.
  • Qabul holatida notified: true xabarnoma yuborishga qo‘yilganini bildiradi. WhatsApp/Telegram’ga yetkazilishini xizmat tasdiqlamaydi.
JSON
{
  "phone": "+77001234567",
  "courseId": 1234,
  "notify": false
}

Limitlar

Cheklovlar platformani va talabalaringizni integratsiyadagi tasodifiy sikllardan himoya qiladi.

CheklovQiymatOshib ketsa
So‘rovlarkalitga daqiqasiga 60429 rate_limited va Retry-After sarlavhasi (takrorlashgacha soniyalar)
Qabullarmaktabga sutkasiga 300429 daily_limit
So‘rov tanasi hajmi4 KB gacha422 payload_too_large
Faol kalitlarmaktabga 3 tagachaKeraksiz kalitni bekor qilib, yangisini yarating

So‘rovlar limiti oshib ketsa, javobda Retry-After sarlavhasi bo‘ladi — takrorlashgacha soniyalar soni. Bugungi foydalanish kabinetdagi «Maktab API» kartochkasida ko‘rinadi.

Xatolar

Barcha xatolar kod va tavsifli JSON ko‘rinishida keladi:

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

HTTP holatiga va error maydoniga (mashina kodi) tayaning; message matni jurnallar uchun mo‘ljallangan.

KodHTTPMa’nosiNima qilish kerak
bad_key401Kalit berilmagan, noto‘g‘ri yoki bekor qilinganX-School-Key sarlavhasini tekshiring; kerak bo‘lsa, kabinetda yangi kalit yarating
key_expired401Kalitning amal qilish muddati tugagan («gacha amal qiladi»)Kabinetda yangi kalit yarating
tariff_not_allowed403Maktab tarifi API’ga kirishni bermaydi (Pro yillik, Lifetime yoki Enterprise kerak). Har bir so‘rovda tekshiriladiMos tarifga o‘ting
course_not_found404Kurs sizning maktabingizda topilmadi, jumladan boshqa maktabga tegishli kurscourseId ni GET /courses javobi bilan solishtiring
validation_error422So‘rov ma’lumotlari noto‘g‘ri (maydon message da ko‘rsatiladi)So‘rovni message tavsifiga ko‘ra tuzating
invalid_json422So‘rov tanasi yaroqli JSON emasJSON va Content-Type sarlavhasini tekshiring
payload_too_large422So‘rov tanasi 4 KB dan kattaSo‘rov tanasini qisqartiring
idempotency_key_required422POST /enrollments uchun Idempotency-Key berilmaganSarlavhani qo‘shing, masalan buyurtma raqami bilan
idempotency_conflict409Bu Idempotency-Key so‘rovning boshqa tanasi bilan ishlatilganYangi operatsiya uchun yangi kalit ishlating yoki asl tanani yuboring
idempotency_in_progress409Shu kalitli so‘rov hali bajarilmoqdaKuting va shu kalit bilan takrorlang
course_locked409Kurs muzlatilganKursni kabinetda muzlatishdan chiqaring yoki boshqasini tanlang
course_closed409Kurs e’lon qilinmagan (masalan, qoralama)Kursni e’lon qiling
student_limit_reached409Tarif bo‘yicha talabalar limitiga yetildiJoy bo‘shating yoki limiti yuqori tarifga o‘ting
rate_limited429Kalitga daqiqasiga 60 tadan ortiq so‘rovRetry-After soniya kutib, takrorlang
daily_limit429Maktabga sutkasiga 300 tadan ortiq qabulKeyinroq takrorlang, Retry-After ga tayaning

failed holatidagi qabul uchun error maydonida not_confirmed bo‘ladi: 15 daqiqa ichida qabul tasdiqlanmadi.

Maxfiylik

API faqat sizning maktabingiz ma’lumotlarini ko‘rsatadi.

  • POST /students javobidagi created: true talaba sizning maktabingizda hali bo‘lmaganini bildiradi — uning akkaunti platformada boshqa maktabda mavjud bo‘lsa ham. Maktabingiz uchun bog‘lanish yaratiladi, begona ma’lumotlar ochilmaydi.
  • created: false — talaba sizning maktabingizda allaqachon o‘qiydi. Mavjud talabaning ismi va pochtasi qayta yozilmaydi.
  • Maktabingiz bilan bog‘lanmagan talaba uchun kirishni tekshirish mavjud bo‘lmagan talaba bilan bir xil javob beradi: active: false.
  • GET /courses faqat kalit egasi maktabining kurslarini qaytaradi; begona kurs 404 course_not_found javobini beradi.