HEBE ACADEMY
Dành cho lập trình viên · v1

API Document – HEBE ACADEMY

REST API dùng JSON, địa chỉ gốc https://hoc.hebeacademy.vn/api/v1, xác thực bằng API key. Dành cho lập trình viên và người dựng luồng tự động (n8n, Make, Google Apps Script).

Mục lục

1. Xác thực và quyền

Mọi yêu cầu cần header Authorization: Bearer <API key>; yêu cầu có body gửi thêm Content-Type: application/json.

GET /api/v1/me HTTP/1.1
Host: hoc.hebeacademy.vn
Authorization: Bearer elc_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Lấy API key

  1. Đăng nhập tài khoản Quản trị → Admin → API & tích hợp.
  2. Đặt tên key, tích các quyền cần dùng, bấm Tạo API key.
  3. Chép key ngay. Key có dạng elc_ + 43 ký tự, chỉ hiện một lần; hệ thống chỉ lưu mã băm SHA-256.
  4. Không dùng nữa hoặc nghi bị lộ: bấm Thu hồi, key ngừng hoạt động ngay. Quyền của key đã tạo không sửa được; cần thêm quyền thì tạo key mới.

Bảng quyền

Quyền Cho phép
courses:read Xem khoá học, chương, bài học, chương trình
courses:write Tạo, sửa, xoá khoá học, chương, bài học, chương trình
users:read Xem người dùng
users:write Tạo, sửa người dùng
enrollments:read Xem ai đang học khoá nào và tiến độ
enrollments:write Cấp, thu hồi quyền học
orders:read Xem đơn hàng
orders:write Xác nhận, huỷ đơn hàng
classes:read Xem lớp offline
classes:write Thêm học viên vào lớp offline

Kiểm tra key: GET /me (không cần quyền nào) trả về tên key, 10 ký tự đầu và danh sách quyền.

{ "data": { "name": "n8n đồng bộ bài học", "prefix": "elc_So848D", "scopes": ["courses:read", "courses:write"] } }

Giới hạn cố định: API không tạo và không sửa được tài khoản ADMIN, dù key có quyền nào. API dành cho gọi từ server; trình duyệt gọi trực tiếp sẽ bị chặn CORS, và không bao giờ để key trong mã chạy trên trình duyệt.

2. Quy ước chung

Kết quả luôn nằm trong data; lỗi luôn nằm trong error. Ngày giờ trả về theo chuẩn ISO 8601 (UTC), số tiền là số nguyên đơn vị đồng.

Gọi tên đối tượng trên đường dẫn

Tham số Nhận Ví dụ
{course} id hoặc slug quan-tri-dong-tien
{category} id hoặc slug tai-chinh
{user} id hoặc email an@gmail.com
{order} mã đơn (không phân biệt hoa thường) DHHU37B771
{class} id hoặc mã lớp (không phân biệt hoa thường) CEO-K01
{chapter}, {lesson} id lấy từ GET /courses/{course}/curriculum

Phân trang (các endpoint danh sách): ?limit= mặc định 50, tối đa 200; ?offset= mặc định 0. Kết quả kèm lại limit và offset; lấy trang tiếp khi số dòng trả về bằng limit.

Dạng lỗi

{ "error": { "code": "bad_request", "message": "Dữ liệu không hợp lệ.", "details": [ { "path": "title", "message": "Thiếu tên khoá học" } ] } }
Mã HTTP code Khi nào
200 — Thành công; hoặc tạo lại đối tượng đã có (trả bản cũ)
201 — Đã tạo mới
400 bad_request Body không phải JSON, thiếu trường, sai kiểu; details ghi từng trường sai
401 unauthorized Thiếu key, key sai hoặc đã thu hồi
403 forbidden Key thiếu quyền, hoặc động vào tài khoản ADMIN
404 not_found Không có đối tượng hoặc endpoint
405 method_not_allowed Sai phương thức; message liệt kê phương thức được phép
409 conflict Trùng slug hoặc email, hoặc đơn không còn ở trạng thái chờ; khi trùng slug, details kèm id của đối tượng đã có
500 internal_error Lỗi hệ thống; thử lại sau

Gọi lại an toàn: POST /users, cấp quyền học, thêm học viên vào lớp và PUT …/curriculum có thể gọi lại nhiều lần với cùng dữ liệu mà không tạo trùng.

3. Khoá học và chương trình

Phương thức Đường dẫn Quyền Việc
GET /courses courses:read Danh sách; lọc ?q= (tên, slug), ?published=true|false, ?category=
POST /courses courses:write Tạo khoá, có thể kèm chapters
GET /courses/{course} courses:read Chi tiết; thêm ?include=curriculum để lấy giáo trình
PATCH /courses/{course} courses:write Sửa, chỉ gửi trường cần đổi
DELETE /courses/{course} courses:write Xoá khoá, kèm ghi danh và tiến độ học viên
GET /categories courses:read Danh sách chương trình
POST /categories courses:write Tạo chương trình: name, slug?, description?, imageUrl?, sort?
PATCH /categories/{category} courses:write Sửa chương trình
DELETE /categories/{category} courses:write Xoá chương trình; các khoá thuộc chương trình đó còn lại, không thuộc chương trình nào

Trường dữ liệu của khoá học

Trường Kiểu Ghi chú
title chuỗi Bắt buộc khi tạo
slug chuỗi Bỏ trống thì tự tạo từ tên, bỏ dấu tiếng Việt. Trùng → 409
summary, description chuỗi Mô tả ngắn và giới thiệu chi tiết
outcomes mảng chuỗi "Bạn sẽ học được"; trả về luôn dạng mảng
coverUrl URL hoặc null Ảnh bìa, tỉ lệ 16:9
price, salePrice số nguyên (đ) salePrice = null là không khuyến mãi
mode ONLINE / OFFLINE / HYBRID Mặc định ONLINE
level chuỗi Mặc định "Cơ bản"
published boolean Mặc định false (nháp). true thì học viên mới thấy và mua được
featured boolean Hiện ở "Khoá học nổi bật" trên trang chủ
category slug/id hoặc null Chương trình phải có sẵn
instructorEmail email hoặc null Tài khoản phải có vai trò Giảng viên, Nhân viên hoặc Quản trị

Ví dụ: tạo khoá

curl -X POST https://hoc.hebeacademy.vn/api/v1/courses \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{
    "title": "Quản trị dòng tiền cho chủ doanh nghiệp",
    "summary": "Đọc được dòng tiền, lập kế hoạch 13 tuần.",
    "price": 1990000, "salePrice": 1490000,
    "category": "tai-chinh", "instructorEmail": "giangvien@gmail.com",
    "outcomes": ["Đọc báo cáo dòng tiền", "Lập kế hoạch 13 tuần"],
    "published": true
  }'

Kết quả (201):

{ "data": {
  "id": "887ca0b8-…", "slug": "quan-tri-dong-tien-cho-chu-doanh-nghiep",
  "title": "Quản trị dòng tiền cho chủ doanh nghiệp", "price": 1990000, "salePrice": 1490000,
  "mode": "ONLINE", "published": true, "featured": false,
  "category": { "id": "…", "slug": "tai-chinh", "name": "Tài chính" },
  "instructor": { "email": "giangvien@gmail.com", "name": "…" },
  "outcomes": ["Đọc báo cáo dòng tiền", "Lập kế hoạch 13 tuần"],
  "curriculum": []
} }

4. Giáo trình: chương và bài học

Cách khuyên dùng là gửi cả giáo trình mỗi lần bằng PUT /courses/{course}/curriculum. Hệ thống khớp theo tên nên gọi lại bao nhiêu lần cũng an toàn, không mất tiến độ học viên.

Phương thức Đường dẫn Việc
GET /courses/{course}/curriculum Xem giáo trình kèm id chương, bài (courses:read)
PUT /courses/{course}/curriculum Đồng bộ cả giáo trình
POST /courses/{course}/chapters Thêm 1 chương vào cuối: title, lessons?, sort?
PATCH /chapters/{chapter} Đổi tên hoặc thứ tự chương: title?, sort?
DELETE /chapters/{chapter} Xoá chương cùng mọi bài trong đó
POST /chapters/{chapter}/lessons Thêm 1 bài vào cuối chương
PATCH /lessons/{lesson} Sửa bài; chapterId để chuyển sang chương khác
DELETE /lessons/{lesson} Xoá bài (mất tiến độ của bài đó)

Các endpoint ghi ở bảng trên đều cần quyền courses:write.

Trường của bài học

Trường Kiểu Ghi chú
title chuỗi Bắt buộc; dùng để khớp khi đồng bộ
videoUrl URL hoặc null YouTube (mọi dạng link), Vimeo hoặc file .mp4
content chuỗi Ghi chú, tài liệu hiện dưới video; giữ xuống dòng
durationMin số nguyên Thời lượng, phút
isPreview boolean true = cho học thử miễn phí

Cách đồng bộ hoạt động (mode: "sync", mặc định)

  1. Chương khớp theo tên (không phân biệt hoa thường, bỏ khoảng trắng thừa); bài khớp theo tên trong cùng chương.
  2. Bài đã có được cập nhật các trường gửi lên và giữ nguyên id, nên học viên không mất tiến độ. Trường không gửi thì giữ giá trị cũ.
  3. Chương, bài chưa có được thêm mới. Thứ tự theo đúng thứ tự trong danh sách gửi lên.
  4. Chương, bài có trên web mà không có trong danh sách: giữ nguyên, trừ khi gửi "prune": true thì xoá.

mode: "append" luôn thêm các chương gửi lên vào cuối, không khớp gì. Đổi tên bài qua đồng bộ sẽ tạo bài mới; muốn đổi tên mà giữ tiến độ thì dùng PATCH /lessons/{lesson}.

Ví dụ

curl -X PUT https://hoc.hebeacademy.vn/api/v1/courses/quan-tri-dong-tien-cho-chu-doanh-nghiep/curriculum \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{
    "prune": false,
    "chapters": [
      { "title": "Nền tảng", "lessons": [
        { "title": "Vì sao doanh nghiệp có lãi vẫn chết", "videoUrl": "https://youtu.be/xxxxxxxxxxx", "durationMin": 12, "isPreview": true },
        { "title": "Ba báo cáo tài chính cần đọc", "videoUrl": "https://youtu.be/yyyyyyyyyyy", "content": "Tài liệu kèm: …" }
      ]},
      { "title": "Lập kế hoạch dòng tiền", "lessons": [
        { "title": "Kế hoạch 13 tuần", "videoUrl": "https://youtu.be/zzzzzzzzzzz", "durationMin": 18 }
      ]}
    ]
  }'

Kết quả (200) báo số thay đổi và giáo trình sau khi đồng bộ:

{ "data": {
  "stats": { "chaptersCreated": 1, "chaptersUpdated": 1, "chaptersDeleted": 0, "lessonsCreated": 1, "lessonsUpdated": 2, "lessonsDeleted": 0 },
  "curriculum": [ { "id": "…", "title": "Nền tảng", "sort": 0, "lessons": [ { "id": "…", "title": "Vì sao doanh nghiệp có lãi vẫn chết", "videoUrl": "…", "durationMin": 12, "isPreview": true, "sort": 0 } ] } ]
} }

5. Người dùng

Phương thức Đường dẫn Quyền Việc
GET /users users:read Danh sách; lọc ?email=, ?role=, ?q= (tên, email, số điện thoại)
POST /users users:write Tạo người dùng
GET /users/{user} users:read Chi tiết theo id hoặc email
PATCH /users/{user} users:write Sửa

Trường dữ liệu

Trường Kiểu Ghi chú
email email Bắt buộc khi tạo; tự chuyển về chữ thường
name chuỗi Bỏ trống thì lấy phần trước @ của email
phone chuỗi
role STUDENT / INSTRUCTOR / STAFF Mặc định STUDENT; không gán được ADMIN
password chuỗi ≥ 6 ký tự Bỏ trống thì tài khoản chưa có mật khẩu, người dùng đăng nhập bằng Google
title, bio, avatarUrl chuỗi / URL Chức danh, giới thiệu, ảnh đại diện (dùng cho giảng viên)
active boolean Chỉ khi sửa: false = khoá tài khoản

Tạo người dùng: email chưa có → 201, tạo mới. Email đã có → 200 kèm "existing": true, trả tài khoản cũ và không sửa gì; muốn cập nhật thì gọi PATCH.

curl -X POST https://hoc.hebeacademy.vn/api/v1/users \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "email": "hocvien@gmail.com", "name": "Trần Thị B", "phone": "0901234567" }'
{ "data": {
  "id": "…", "email": "hocvien@gmail.com", "name": "Trần Thị B", "phone": "0901234567",
  "role": "STUDENT", "title": null, "bio": null, "avatarUrl": null, "active": true,
  "googleLinked": false, "hasPassword": false, "createdAt": "2026-09-26T08:00:00.000Z"
}, "existing": false }

googleLinked = đã từng đăng nhập bằng Google; hasPassword = đã có mật khẩu. Sửa tài khoản ADMIN trả 403; đổi sang email đã có người dùng trả 409.

6. Cấp quyền học

Cấp quyền theo email, kể cả người chưa có tài khoản: hệ thống tạo sẵn tài khoản, người học bấm "Đăng nhập với Google" đúng email đó là vào học.

Phương thức Đường dẫn Quyền Việc
POST /courses/{course}/enrollments enrollments:write Cấp cho 1 người hoặc danh sách (tối đa 500)
DELETE /courses/{course}/enrollments/{user} enrollments:write Thu hồi; {user} là id hoặc email
GET /courses/{course}/enrollments enrollments:read Ai đang học khoá và tiến độ từng người
GET /users/{user}/enrollments enrollments:read Các khoá một người đang học

Body khi cấp: một người { "email", "name"?, "phone"? }, hoặc danh sách { "users": [ { "email", "name"? }, … ] }.

curl -X POST https://hoc.hebeacademy.vn/api/v1/courses/quan-tri-dong-tien-cho-chu-doanh-nghiep/enrollments \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "users": [ { "email": "a@gmail.com", "name": "Nguyễn Văn A" }, { "email": "b@gmail.com" } ] }'

Kết quả (201), mỗi email một dòng. status là granted (vừa cấp) hoặc already (đã có quyền từ trước):

{ "data": [
    { "email": "a@gmail.com", "userId": "…", "userCreated": true, "status": "granted" },
    { "email": "b@gmail.com", "userId": "…", "userCreated": false, "status": "already" }
  ],
  "summary": { "granted": 1, "already": 1, "usersCreated": 1 } }

Tiến độ trả về ở GET /courses/{course}/enrollments, mỗi dòng có user, enrolledAt và progress: { "done": 3, "total": 12 } (số bài đã hoàn thành / tổng số bài). Thu hồi người không có quyền trả 404.

7. Đơn hàng và lớp offline

Phương thức Đường dẫn Quyền Việc
GET /orders orders:read Danh sách, mới nhất trước; lọc ?status=PENDING|PAID|CANCELLED, ?since= (ngày giờ ISO, lấy đơn tạo hoặc thanh toán từ thời điểm đó)
GET /orders/{order} orders:read Chi tiết đơn
POST /orders/{order}/confirm orders:write Xác nhận đã thu tiền: ghi danh khoá hoặc thêm vào lớp, giống nút Xác nhận trong admin
POST /orders/{order}/cancel orders:write Huỷ đơn đang chờ
GET /classes classes:read Danh sách lớp kèm sĩ số hiện tại; lọc ?status=UPCOMING|ONGOING|FINISHED|CANCELLED
GET /classes/{class} classes:read Chi tiết lớp, danh sách học viên, học phí, đã đóng, còn nợ
POST /classes/{class}/students classes:write Thêm học viên: email, name?, phone?, fee? (mặc định = học phí của lớp)

Xác nhận hoặc huỷ đơn không còn ở trạng thái PENDING trả 409. Đơn chuyển khoản thường đã được GPM Pay tự xác nhận, nên confirm chủ yếu dùng cho đơn tiền mặt hoặc thu qua kênh khác.

Một đơn hàng trả về

{ "data": {
  "code": "DHHU37B771", "status": "PAID", "method": "BANK",
  "amount": 10000, "discount": 0, "total": 10000, "couponCode": null, "note": null,
  "user": { "id": "…", "email": "hocvien@gmail.com", "name": "…" },
  "course": { "slug": "…", "title": "…" }, "class": null,
  "createdAt": "2026-09-26T03:38:10.000Z", "paidAt": "2026-09-26T03:39:14.000Z"
} }

method: BANK (chuyển khoản), CASH (tiền mặt), FREE (đơn 0đ). course hoặc class có giá trị tuỳ đơn mua khoá online hay đăng ký lớp.

Đồng bộ đơn sang hệ thống khác: gọi GET /orders?status=PAID&since=<lần gọi trước> theo chu kỳ (ví dụ mỗi 5 phút), lưu lại thời điểm gọi, dùng code làm khoá chống trùng.

8. Ví dụ tích hợp

Google Sheets → cấp khoá học. Sheet có cột A = email, B = họ tên, C = slug khoá. Dán vào Extensions → Apps Script, lưu key vào Project Settings → Script properties với tên ELC_KEY, rồi chạy capKhoaHoc. Kết quả ghi vào cột D.

const BASE = 'https://hoc.hebeacademy.vn/api/v1';

function capKhoaHoc() {
  const key = PropertiesService.getScriptProperties().getProperty('ELC_KEY');
  const sheet = SpreadsheetApp.getActiveSheet();
  const rows = sheet.getDataRange().getValues();
  for (let i = 1; i < rows.length; i++) {            // bỏ dòng tiêu đề
    const [email, name, course, done] = rows[i];
    if (!email || !course || done) continue;          // đã xử lý thì bỏ qua
    const res = UrlFetchApp.fetch(`${BASE}/courses/${course}/enrollments`, {
      method: 'post',
      contentType: 'application/json',
      headers: { Authorization: `Bearer ${key}` },
      payload: JSON.stringify({ email, name }),
      muteHttpExceptions: true,
    });
    const body = JSON.parse(res.getContentText());
    const result = res.getResponseCode() < 300 ? body.data[0].status : `Lỗi: ${body.error.message}`;
    sheet.getRange(i + 1, 4).setValue(result);
  }
}

n8n. Dùng node HTTP Request:

Mục Giá trị
Authentication Generic Credential Type → Header Auth
Header Auth Name Authorization, Value Bearer elc_… (lưu trong Credentials, không dán thẳng vào node)
Method / URL Ví dụ POST + https://hoc.hebeacademy.vn/api/v1/courses/{{$json.courseSlug}}/enrollments
Body JSON, ví dụ { "email": "{{$json.email}}", "name": "{{$json.name}}" }
Settings Bật Retry On Fail (3 lần) để chịu lỗi mạng tạm thời; các lệnh cấp và đồng bộ gọi lại đều an toàn

Luồng gợi ý: Form đăng ký / đơn hàng bên ngoài → POST /users → POST /courses/{slug}/enrollments → gửi email chào mừng kèm link https://hoc.hebeacademy.vn/login.

Đẩy giáo trình từ file JSON (soạn giáo trình trong giao-trinh.json theo mẫu ở mục 4, sau mỗi lần sửa chỉ cần chạy lại):

export KEY=elc_xxx
curl -sS -X PUT https://hoc.hebeacademy.vn/api/v1/courses/quan-tri-dong-tien-cho-chu-doanh-nghiep/curriculum \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  --data @giao-trinh.json | jq '.data.stats // .error'

Giữ an toàn key: mỗi hệ thống một key riêng, chỉ đủ quyền cần dùng; không đưa key vào sheet dùng chung, mã nguồn công khai hay tin nhắn. Kiểm tra cột "Dùng lần cuối" trong Admin → API & tích hợp để phát hiện key bị dùng bất thường.