Tìm kiếm tài liệu...
Mẹo tìm kiếm
Hỏi bằng tiếng Việt. AI sẽ tìm đúng nội dung trong docs cho bạn.

Hướng dẫn tích hợp API Chia sẻ học liệu

Tài liệu tích hợp Partner API cho Attachments và Learning Resources trên Trục LMS Hà Nội.


Tài liệu này dành cho partner tích hợp lần đầu API chia sẻ học liệu trên Trục LMS (bao gồm luồng upload tệp đính kèm và chia sẻ học liệu).

Chọn nhánh triển khai

Chọn đúng tài liệu theo loại đơn vị để triển khai nhanh và giảm nhầm lẫn:

Đơn vị tích hợpTài liệu nên đọc trướcEndpoint học liệuGhi chú chính
Nhà trườngTích hợp cho Nhà trườngPOST /api/lms-base/learning-resourcesKiểm tra chặt authorPinCode/creatorPinCode theo ngữ cảnh dữ liệu Sở
Tổ chứcTích hợp cho Tổ chứcPOST /api/lms-base/learning-resources/organizationsUpsert theo partnerId + sourceId; có thể để trống thông tin người chia sẻ để hệ thống tự điền
SởTham chiếu tài liệu tổng quan trang nàyPOST /api/lms-base/learning-resources/doetsUpsert theo scope Sở; hệ thống tự gán tác giả/người chia sẻ theo mã và tên Sở
Xã/phườngTham chiếu tài liệu tổng quan trang nàyPOST /api/lms-base/learning-resources/divisionsUpsert theo scope xã/phường; hệ thống tự gán tác giả/người chia sẻ theo mã và tên xã/phường

Trang này giữ vai trò tài liệu tổng quan + reference dùng chung cho cả 4 nhánh. Hướng dẫn tích hợp API tải học liệu đã duyệt: Tải học liệu đã duyệt.

Tích hợp nhanh

Happy path chuẩn:

  1. Upload file qua POST /api/lms-base/attachments.
  2. Nhận idpath cho từng file.
  3. Replace URL trong HTML (src/href/poster) bằng path đã upload.
  4. Gọi API upsert học liệu theo đúng scope với fileIds khớp đúng các file đang dùng.
  5. Poll trạng thái duyệt qua endpoint danh sách tương ứng scope:
    • Nhà trường: GET /api/lms-base/learning-resources
    • Tổ chức: GET /api/lms-base/learning-resources/organizations
    • Sở: GET /api/lms-base/learning-resources/doets
    • Xã/phường: GET /api/lms-base/learning-resources/divisions

Lưu ý quan trọng:

  • POST /learning-resources đang xử lý theo cơ chế upsert theo cặp schoolCode + sourceId trong phạm vi partner gọi API (X-Api-Key): nếu đã có bản ghi Pending/Rejected thì cập nhật, nếu chưa có thì tạo mới, nếu đã Approved thì bị từ chối.
  • Nếu chia sẻ theo scope tổ chức, dùng POST /learning-resources/organizations; cơ chế upsert theo cặp partnerId + sourceId.
  • Nếu chia sẻ theo scope Sở, dùng POST /learning-resources/doets; cơ chế upsert theo sourceId trong phạm vi partner + doetCode.
  • Nếu chia sẻ theo scope xã/phường, dùng POST /learning-resources/divisions; cơ chế upsert theo sourceId trong phạm vi partner + doetCode + divisionCode.
  • authorPinCodecreatorPinCode với endpoint POST /learning-resources phải là CCCD 12 số, tồn tại đúng ngữ cảnh dữ liệu Sở (doetCode/divisionCode/schoolCode/yearCode).
  • gradeCode/subjectCode/knowledgeUnitCode phải map theo danh mục dùng chung: https://docs.hanoi.edu.vn/docs/knowledge-units.
  • Nếu truyền fileIds, tất cả fileIds bắt buộc phải thực sự xuất hiện trong thumbnailUrl hoặc contents.
  • Theo dõi trạng thái duyệt: ưu tiên dùng webhook (Trục chủ động gọi về partner khi đổi trạng thái — xem mục 7.6); có thể dùng polling API danh sách để đối soát/dự phòng.

1. Tổng quan bài toán

Bài toán tích hợp gồm 2 phần:

  1. Quản lý file dùng trong học liệu (attachments).
  2. Tạo bản ghi học liệu số (learning resource) và theo dõi duyệt.

Thông tin kết nối:

  • Endpoint theo môi trường:
    • Môi trường Production (PROD): https://partner.hanoi.edu.vn/
    • Môi trường Development (DEV): https://lmsbpartnerapi.vstudy.edu.vn/
  • Auth header: X-Api-Key: <api-key-don-vi-duoc-cap>
  • JSON API: Content-Type: application/json
  • Upload file: multipart/form-data cho /api/lms-base/attachments

Lưu ý: các ví dụ curl trong tài liệu này đang dùng endpoint môi trường Development (DEV). Khi triển khai thật, đổi sang môi trường Production (PROD).

Liên hệ cấp X-Api-Key

Thông tin dưới đây chỉ dùng cho việc cấp API key tích hợp Partner API:

Đơn vịNgười phụ tráchSố điện thoạiEmail
QIGNguyễn Thành Luân0988933774thanhluan@qig.vn

Format response chung:

{
  "success": true,
  "message": "Mô tả kết quả",
  "data": {}
}
json

2. Luồng tích hợp end-to-end

  1. Chuẩn bị metadata học liệu (sourceId, mã phân loại, thông tin tác giả/người gửi).
  2. Upload các file/media sẽ được tham chiếu trong học liệu (ảnh đại diện nếu dùng ảnh đã upload, file trong contents, SCORM .zip nếu có). Với YOUTUBE, IFRAME hoặc thumbnailUrl dùng URL ngoài hợp lệ, không cần upload file tương ứng.
  3. Lưu lại id + path từ response upload.
  4. Replace URL trong contents[].content bằng path tương ứng; thumbnailUrl có thể dùng URL ảnh đã upload hoặc URL ảnh ngoài hợp lệ.
  5. Tạo payload học liệu với fileIds khớp đúng file đang dùng.
  6. Gọi API chia sẻ học liệu (upsert) theo scope phù hợp:
    • POST /api/lms-base/learning-resources (scope trường)
    • POST /api/lms-base/learning-resources/organizations (scope tổ chức)
    • POST /api/lms-base/learning-resources/doets (scope Sở)
    • POST /api/lms-base/learning-resources/divisions (scope xã/phường)
  7. Poll API danh sách theo scope để theo dõi status:
    • Nhà trường: GET /api/lms-base/learning-resources
    • Tổ chức: GET /api/lms-base/learning-resources/organizations
    • Sở: GET /api/lms-base/learning-resources/doets
    • Xã/phường: GET /api/lms-base/learning-resources/divisions
  8. Khi học liệu đã được duyệt và đủ điều kiện khai thác, tích hợp API tải theo tài liệu: Tải học liệu đã duyệt.
  9. Nếu cần, kiểm tra trạng thái từng file bằng GET /api/lms-base/attachments/{attachmentId}.
  10. Nếu cần thu hồi bản ghi đang chờ duyệt, gọi API xóa theo scope:
  • Nhà trường: DELETE /api/lms-base/learning-resources
  • Tổ chức: DELETE /api/lms-base/learning-resources/organizations
  • Sở: DELETE /api/lms-base/learning-resources/doets
  • Xã/phường: DELETE /api/lms-base/learning-resources/divisions

3. Checklist trước khi gọi API

Checklist bắt buộc:

  • Đã có X-Api-Key hợp lệ.
  • Đã map đúng mã dùng chung của Sở (gradeCode, subjectCode, knowledgeUnitCode) từ danh mục knowledge-units.
  • Với endpoint /learning-resources, đã xác thực authorPinCode/creatorPinCode đúng người, đúng ngữ cảnh (doetCode/divisionCode/schoolCode/yearCode).
  • Đã chọn đúng endpoint theo scope chia sẻ:
    • POST /learning-resources: upsert theo schoolCode + sourceId trong phạm vi partner gọi API (X-Api-Key).
    • POST /learning-resources/organizations: upsert theo partnerId + sourceId.
    • POST /learning-resources/doets: upsert theo sourceId trong phạm vi partner + doetCode.
    • POST /learning-resources/divisions: upsert theo sourceId trong phạm vi partner + doetCode + divisionCode.
    • Poll duyệt/xóa pending cũng theo đúng scope tương ứng.
  • Nếu sourceId đã có bản ghi Approved, phải dùng sourceId mới để chia sẻ tiếp (không cập nhật đè).
  • File upload đúng loại MIME cho mục đích sử dụng.
  • Giới hạn request upload của API là <= 1073741824 byte (1 GB); từng định dạng tệp vẫn có thể bị giới hạn thấp hơn theo cấu hình hệ thống.
  • Tất cả URL trong thumbnailUrlcontents là URL tuyệt đối hợp lệ (http/https).
  • Nếu có fileIds, mỗi id phải là ULID 26 ký tự, thuộc đúng partner và thực sự được dùng trong thumbnailUrl hoặc contents.
  • HTML content không chứa thành phần bị cấm (CSS/JS/iframe/object/embed/on*/protocol không an toàn).

4. Quickstart với 1 ví dụ đầy đủ

4.1. Nội dung HTML ban đầu (before replace)

<p>Xem video minh họa:</p>
<video controls poster="https://partner.example.com/media/thumb-phan-so.png">
  <source src="https://partner.example.com/media/video-phan-so.mp4" type="video/mp4" />
</video>
<p>Tải tài liệu PDF: <a href="https://partner.example.com/media/phieu-bai-tap.pdf">Tại đây</a></p>
html

4.2. Upload file như thế nào

Upload từng file riêng qua POST /api/lms-base/attachments.

curl --location 'https://lmsbpartnerapi.vstudy.edu.vn/api/lms-base/attachments' \
  --header 'X-Api-Key: <api-key-don-vi-duoc-cap>' \
  --form 'doetCode="01"' \
  --form 'divisionCode="0001"' \
  --form 'schoolCode="01000001"' \
  --form 'educationLevelCode="02"' \
  --form 'file=@"/path/to/thumb-phan-so.png"'
bash
{
  "success": true,
  "message": "Tải tệp thành công.",
  "data": {
    "id": "01JQ2S96MKZ8E8GHW8EXB4VPA7",
    "path": "https://<cdn-or-storage>/partner-code/thumb-phan-so.png",
    "useStatus": "Draft"
  }
}
json

Tương tự upload video và pdf (mỗi file 1 request), ví dụ nhận:

  • Video: id=01JQ2SAXA9QXRVQ8S9QPA4J2AN, path=https://<cdn-or-storage>/partner-code/video-phan-so.mp4
  • PDF: id=01JQ2SB6R52J01Q2RC9F0M6V7P, path=https://<cdn-or-storage>/partner-code/phieu-bai-tap.pdf

4.3. HTML sau khi replace URL (after replace)

<p>Xem video minh họa:</p>
<video controls poster="https://<cdn-or-storage>/partner-code/thumb-phan-so.png">
  <source src="https://<cdn-or-storage>/partner-code/video-phan-so.mp4" type="video/mp4" />
</video>
<p>Tải tài liệu PDF: <a href="https://<cdn-or-storage>/partner-code/phieu-bai-tap.pdf">Tại đây</a></p>
html

4.4. Payload cuối cùng gửi lên learning-resources

{
  "sourceId": "LR-HN-000001",
  "title": "Bài học: So sánh phân số",
  "thumbnailUrl": "https://<cdn-or-storage>/partner-code/thumb-phan-so.png",
  "authorPinCode": "001095123456",
  "authorName": "Nguyen Van A",
  "creatorPinCode": "001095123456",
  "creatorName": "Nguyen Van A",
  "fileIds": [
    "01JQ2S96MKZ8E8GHW8EXB4VPA7",
    "01JQ2SAXA9QXRVQ8S9QPA4J2AN",
    "01JQ2SB6R52J01Q2RC9F0M6V7P",
    "01JQ2SCF3X7VY7S5M9N0B8K4Q1"
  ],
  "contents": [
    {
      "name": "Mở đầu",
      "displayOrder": 1,
      "type": "TEXT",
      "content": "<p>Xem video minh họa:</p><video controls poster=\"https://<cdn-or-storage>/partner-code/thumb-phan-so.png\"><source src=\"https://<cdn-or-storage>/partner-code/video-phan-so.mp4\" type=\"video/mp4\" /></video><p>Tải tài liệu PDF: <a href=\"https://<cdn-or-storage>/partner-code/phieu-bai-tap.pdf\">Tại đây</a></p>",
      "description": "Mở đầu"
    },
    {
      "name": "Video tải lên",
      "displayOrder": 2,
      "type": "VIDEO",
      "content": "https://<cdn-or-storage>/partner-code/video-phan-so-2.mp4",
      "description": "Video chính của học liệu"
    },
    {
      "name": "Video YouTube tham khảo",
      "displayOrder": 3,
      "type": "YOUTUBE",
      "content": "https://www.youtube.com/watch?v=d3bn7DVrZOw",
      "description": "Video tham khảo"
    }
  ],
  "gradeCode": "06",
  "subjectCode": "0601",
  "knowledgeUnitCode": "201100101",
  "doetCode": "01",
  "divisionCode": "0001",
  "schoolCode": "01000001",
  "yearCode": 2025,
  "educationLevelCode": "02",
  "semesterCode": 1
}
json

Gọi API:

curl --location 'https://lmsbpartnerapi.vstudy.edu.vn/api/lms-base/learning-resources' \
  --header 'X-Api-Key: <api-key-don-vi-duoc-cap>' \
  --header 'Content-Type: application/json' \
  --data '<payload-json-o-tren>'
bash

Response thành công:

{
  "success": true,
  "message": "Tạo mới học liệu số thành công.",
  "data": {
    "sourceId": "LR-HN-000001",
    "id": "01JQ2T5W11Z9J6AEX4Q6F90HME",
    "approvalStatus": 0,
    "operation": "created"
  }
}
json

operation có 2 giá trị:

  • created: tạo mới học liệu.
  • updated: cập nhật bản ghi đã có ở trạng thái Pending hoặc Rejected.

4.5. Poll trạng thái duyệt

curl --location --get 'https://lmsbpartnerapi.vstudy.edu.vn/api/lms-base/learning-resources' \
  --header 'X-Api-Key: <api-key-don-vi-duoc-cap>' \
  --data-urlencode 'approvalStatus=0' \
  --data-urlencode 'doetCode=01' \
  --data-urlencode 'schoolCode=01000001' \
  --data-urlencode 'skip=0' \
  --data-urlencode 'take=20'
bash

Response thành công:

{
  "success": true,
  "message": "Lấy danh sách học liệu thành công.",
  "data": [
    {
      "id": "01JQ2T5W11Z9J6AEX4Q6F90HME",
      "sourceId": "LR-HN-000001",
      "title": "Bài học: So sánh phân số",
      "status": "Pending",
      "reason": null,
      "occurredAt": "2026-04-12T07:12:44.0000000Z",
      "canDelete": true,
      "createdAt": "2026-04-12T07:12:44.0000000Z",
      "updatedAt": "2026-04-12T07:12:44.0000000Z"
    }
  ]
}
json

Trường status trong item response là chuỗi, gồm 5 giá trị: Pending, Approved, Rejected, Hidden, Removed. Khi duyệt/kiểm duyệt xong, status đổi từ Pending sang Approved/Rejected (hoặc về sau có thể là Hidden/Removed); với Rejected/Removed sẽ kèm reason.

Lưu ý: query param approvalStatus để lọc vẫn dùng số (0/1/2), độc lập với trường status dạng chuỗi trong response.

Nếu chia sẻ theo scope khác, poll bằng endpoint tương ứng:

curl --location --get 'https://lmsbpartnerapi.vstudy.edu.vn/api/lms-base/learning-resources/organizations' \
  --header 'X-Api-Key: <api-key-don-vi-duoc-cap>' \
  --data-urlencode 'approvalStatus=0' \
  --data-urlencode 'skip=0' \
  --data-urlencode 'take=20'
bash

Các endpoint polling theo scope:

  • Nhà trường: GET /api/lms-base/learning-resources
  • Tổ chức: GET /api/lms-base/learning-resources/organizations
  • Sở: GET /api/lms-base/learning-resources/doets
  • Xã/phường: GET /api/lms-base/learning-resources/divisions

Response giữa các endpoint có cùng cấu trúc (success, message, data[]).

5. Upload attachments

5.1. Upload tệp

  • Method: POST
  • Path: /api/lms-base/attachments
  • Content-Type: multipart/form-data

Form-data fields:

FieldBắt buộcKiểu dữ liệuKích thước/giới hạnMô tả
doetCodestringTối đa 50 ký tựMã Sở GDĐT
divisionCodeKhôngstring / nullTối đa 50 ký tựMã phường/xã
schoolCodeKhôngstring / nullTối đa 50 ký tựNếu truyền thì phải là mã trường hợp lệ thuộc doetCode; nếu để trống thì hệ thống xử lý theo luồng không gắn trường
educationLevelCodeKhôngstring / nullTối đa 50 ký tựNếu truyền thì phải là mã cấp học hợp lệ. Khi có schoolCode thì bắt buộc truyền và phải khớp cấp học của trường
fileIFormFile (binary)> 0<= 1073741824 byteFile/media cần upload

Ràng buộc kỹ thuật:

  • Kích thước file: > 0<= 1073741824 byte (1 GB).
  • Tệp upload phải có phần mở rộng và phần mở rộng phải nằm trong danh mục hệ thống hỗ trợ.
  • Ngoài giới hạn request 1 GB, hệ thống còn đối chiếu giới hạn dung lượng theo từng phần mở rộng từ cấu hình.
  • MIME được kiểm tra theo chữ ký file (không chỉ extension).
  • MIME được chấp nhận:
  • image: image/jpeg, image/png, image/gif, image/webp.
  • document: application/pdf, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document.
  • spreadsheet: application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.
  • slide: application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.presentation.
  • video: video/mp4, video/webm.
  • audio: audio/mpeg.
  • scorm: application/zip.
  • Nếu không truyền schoolCode: doetCode bắt buộc; divisionCode nếu có phải thuộc doetCode; educationLevelCode nếu có phải là mã cấp học hợp lệ.
  • Nếu truyền schoolCode: hệ thống bắt buộc kiểm tra trường thuộc doetCode; educationLevelCode bắt buộc và phải khớp dữ liệu trường; divisionCode nếu có phải khớp với trường và theo đúng năm học dữ liệu trường.
  • File upload phải được mapping dùng trong thumbnailUrl hoặc contents; file upload không được mapping sử dụng sẽ bị xóa khỏi hệ thống.
  • Cơ chế dọn file không được mapping do hệ thống trục có job tự động quét dọn; partner không cần xử lý luồng này.

5.2. Kiểm tra trạng thái tệp

  • Method: GET
  • Path: /api/lms-base/attachments/{attachmentId}

Ví dụ response:

{
  "success": true,
  "message": "Lấy trạng thái tệp thành công.",
  "data": {
    "useStatus": "Used",
    "ownerId": "01JQ2T5W11Z9J6AEX4Q6F90HME",
    "accessUrl": "https://<cdn-or-storage>/partner-code/uuid.mp4"
  }
}
json

Ý nghĩa useStatus:

  • Draft: file chưa gắn vào học liệu.
  • Used: file đã gắn với một học liệu.

6. Tạo learning resource

6.1. Endpoint

  • Method: POST
  • Path (scope trường): /api/lms-base/learning-resources
  • Path (scope tổ chức): /api/lms-base/learning-resources/organizations
  • Path (scope Sở): /api/lms-base/learning-resources/doets
  • Path (scope xã/phường): /api/lms-base/learning-resources/divisions
  • Content-Type: application/json

Rule upsert theo endpoint:

  • Scope trường (/learning-resources): xác định bản ghi theo schoolCode + sourceId trong phạm vi partner gọi API (X-Api-Key).
  • Scope tổ chức (/learning-resources/organizations): xác định bản ghi theo partnerId + sourceId.
  • Scope Sở (/learning-resources/doets): xác định bản ghi theo sourceId trong phạm vi partner + doetCode.
  • Scope xã/phường (/learning-resources/divisions): xác định bản ghi theo sourceId trong phạm vi partner + doetCode + divisionCode.
  • Nếu đã có bản ghi Approved, API từ chối cập nhật và yêu cầu sourceId mới.

6.2. Các trường request

Quy ước đọc cột Bắt buộc:

  • : bắt buộc cho mọi scope chia sẻ.
  • Có (scope trường): bắt buộc với POST /learning-resources.
  • Có (scope trường, Sở, xã/phường): bắt buộc với 3 scope này; scope tổ chức được tự gán theo rule hệ thống.
  • Có (scope xã/phường): chỉ bắt buộc với POST /learning-resources/divisions.
  • Không: không bắt buộc, nhưng nếu truyền vẫn bị validate định dạng/độ dài.
FieldBắt buộcKiểu dữ liệuKích thước/giới hạnGhi chú
sourceIdstringTối đa 128 ký tựKhóa upsert theo scope: trường dùng schoolCode + sourceId, tổ chức dùng partnerId + sourceId, Sở dùng doetCode + sourceId, xã/phường dùng doetCode + divisionCode + sourceId (đều trong phạm vi partner gọi API). Chỉ chấp nhận văn bản thuần, không chứa HTML
titlestringTối đa 500 ký tựTiêu đề học liệu. Chỉ chấp nhận văn bản thuần, không chứa HTML
descriptionKhôngstring / nullTối đa 1024 ký tựMô tả học liệu dạng văn bản hoặc HTML an toàn (áp dụng rule an toàn nội dung)
thumbnailUrlstringTối đa 1024 ký tự; URL tuyệt đốiBắt buộc. Có thể là path ảnh đã upload hoặc URL ảnh ngoài hợp lệ (.jpg, .jpeg, .png, .gif, .bmp, .webp)
authorPinCodeCó (scope trường)stringTối đa 50 ký tự; scope trường yêu cầu CCCD 12 chữ sốScope trường: bắt buộc CCCD hợp lệ theo ngữ cảnh dữ liệu Sở. Scope tổ chức: có thể để trống, hệ thống tự điền từ thông tin partner. Scope Sở/xã-phường: hệ thống tự gán theo mã đơn vị sở hữu
authorNameCó (scope trường)stringTối đa 256 ký tựScope trường: tên tác giả do đơn vị truyền. Scope tổ chức: có thể để trống để hệ thống tự điền theo tên tổ chức. Scope Sở/xã-phường: hệ thống tự gán theo tên đơn vị sở hữu. Nếu truyền thì chỉ chấp nhận văn bản thuần, không chứa HTML
creatorPinCodeCó (scope trường)stringTối đa 50 ký tự; scope trường yêu cầu CCCD 12 chữ sốScope trường: CCCD người chia sẻ phải hợp lệ theo ngữ cảnh dữ liệu Sở. Scope tổ chức: có thể để trống, hệ thống tự điền từ thông tin partner. Scope Sở/xã-phường: hệ thống tự gán theo mã đơn vị sở hữu
creatorNameCó (scope trường)stringTối đa 256 ký tựScope trường: tên người chia sẻ do đơn vị truyền. Scope tổ chức: có thể để trống để hệ thống tự điền theo tên tổ chức. Scope Sở/xã-phường: hệ thống tự gán theo tên đơn vị sở hữu. Nếu truyền thì chỉ chấp nhận văn bản thuần, không chứa HTML
fileIdsKhôngarray<string>Mỗi phần tử ULID 26 ký tựDanh sách attachment dùng cho thumbnailUrlcontents; nếu truyền thì tất cả phải hợp lệ và thuộc đối tác
contentsarray<object>Ít nhất 1 phần tửDanh sách nội dung học liệu
gradeCodestringTối đa 20 ký tựMã khối theo danh mục định danh của Sở
subjectCodestringTối đa 20 ký tựMã môn học theo danh mục định danh của Sở
knowledgeUnitCodestringTối đa 10 ký tựMã đơn vị kiến thức theo danh mục định danh của Sở
doetCodeCó (scope trường, Sở, xã/phường)stringTối đa 20 ký tựScope trường/Sở/xã-phường: bắt buộc. Scope tổ chức: có thể để trống, hệ thống tự gán cố định 01 theo cấu hình hiện tại
divisionCodeCó (scope xã/phường)string / nullTối đa 20 ký tựScope xã/phường: bắt buộc và phải thuộc doetCode + yearCode. Scope trường/tổ chức/Sở: không bắt buộc
schoolCodeCó (scope trường)stringTối đa 50 ký tựScope trường: bắt buộc và dùng để validate ngữ cảnh. Scope tổ chức/Sở/xã-phường: không bắt buộc và thường để rỗng
yearCodeintTừ 1000 đến 9999Năm học dạng 4 chữ số và phải tồn tại trong danh mục năm học hệ thống
educationLevelCodestringTối đa 20 ký tựMã cấp học
semesterCodeintChỉ nhận 1 hoặc 2Học kỳ

6.3. Mapping mã dùng chung (bắt buộc)

Các trường phân loại học liệu phải dùng mã định danh do Sở quy định.

TrườngYêu cầu mappingNguồn tra cứu
gradeCodeMap đúng mã khối thuộc educationLevelCodehttps://docs.hanoi.edu.vn/docs/knowledge-units
subjectCodeMap đúng mã môn học thuộc gradeCodehttps://docs.hanoi.edu.vn/docs/knowledge-units
knowledgeUnitCodeMap đúng mã đơn vị kiến thức thuộc cặp gradeCode/subjectCodehttps://docs.hanoi.edu.vn/docs/knowledge-units

6.4. Cấu trúc contents[]

FieldBắt buộcKiểu dữ liệuKích thước/giới hạnGhi chú
namestringTối đa 256 ký tựTên nội dung. Chỉ chấp nhận văn bản thuần, không chứa HTML
displayOrderKhôngint / null>= 1; không truyền thì tự gánNếu không truyền, hệ thống tự gán theo thứ tự mảng (bắt đầu từ 1)
typestring (enum)Tối đa 20 ký tựTEXT, DOCUMENT, SPREADSHEET, VIDEO, AUDIO, YOUTUBE, SCORM, SLIDE, IFRAME
contentstringKhông giới hạn độ dài (nvarchar(max))Nội dung theo rule từng type
descriptionKhôngstring / nullTối đa 1024 ký tựMô tả nội dung dạng văn bản hoặc HTML an toàn

Rule content theo type:

typeRule cho content
TEXTVăn bản thuần hoặc HTML; nếu HTML thì URL trong src/href/poster phải là URL tuyệt đối hợp lệ
DOCUMENTURL tuyệt đối; nếu map file upload thì MIME phải là application/pdf, application/msword, hoặc application/vnd.openxmlformats-officedocument.wordprocessingml.document
SPREADSHEETURL tuyệt đối; nếu map file upload thì MIME phải là application/vnd.ms-excel hoặc application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
VIDEOURL tuyệt đối; nếu map file upload thì MIME phải là video/mp4 hoặc video/webm
AUDIOURL tuyệt đối; nếu map file upload thì MIME phải là audio/mpeg
YOUTUBEChỉ chấp nhận URL tuyệt đối dạng https://youtube.com/watch?v=... (bao gồm www.youtube.com/watch?v=...) hoặc https://youtu.be/...; không truyền iframe
SCORMURL tuyệt đối trỏ tới file .zip đã upload; MIME phải là application/zip
SLIDEURL tuyệt đối; nếu map file upload thì MIME phải là application/vnd.ms-powerpoint hoặc application/vnd.openxmlformats-officedocument.presentationml.presentation
IFRAMEURL tuyệt đối (http/https) của trang/công cụ ngoài cần nhúng; domain phải thuộc danh sách cho phép (mặc định: youtube.com, olm.vn, sgk.olm.vn và các subdomain). Không map file upload, không truyền thẻ <iframe> HTML

Định dạng được hỗ trợ theo type:

  • TEXT: nội dung inline.
  • DOCUMENT: pdf, doc, docx.
  • SPREADSHEET: xls, xlsx.
  • VIDEO: mp4, webm.
  • AUDIO: mp3.
  • YOUTUBE: link YouTube tuyệt đối hợp lệ (không áp dụng định dạng file upload).
  • SCORM: zip.
  • SLIDE: ppt, pptx.
  • IFRAME: URL tuyệt đối thuộc domain được phép (không áp dụng định dạng file upload).

Rule an toàn nội dung (bắt buộc):

  • content không được chứa CSS/TypeScript/JavaScript.
  • Trong HTML (TEXT/description) không được dùng thẻ iframe, object, embed. Muốn nhúng trang/công cụ ngoài, dùng nội dung loại IFRAME (truyền URL, không truyền thẻ <iframe>).
  • Không dùng event handler dạng on*.
  • Không dùng protocol/script không an toàn: javascript:, vbscript:, data:text/html, data:application/javascript.
  • Chỉ hỗ trợ SCORM 1.2 hoặc 2004.
  • Nội dung loại IFRAME chỉ chấp nhận URL tuyệt đối (http/https) có domain (hoặc subdomain) thuộc danh sách cho phép cấu hình ở LearningContentIframe:AllowedHosts (mặc định: youtube.com, olm.vn, sgk.olm.vn).
  • Nếu truyền fileIds, tất cả fileIds phải được dùng trong thumbnailUrl hoặc contents; nếu không sẽ bị từ chối.

7. Tra cứu trạng thái duyệt

7.1. Endpoint danh sách học liệu nhà trường

  • Method: GET
  • Path: /api/lms-base/learning-resources
  • Bắt buộc truyền doetCode trong query.

Query params:

FieldKiểu dữ liệuGiới hạnGhi chú
sourceIdstring / nullTối đa 128 ký tựLọc theo mã học liệu nguồn
educationLevelCodestring / nullTối đa 20 ký tựTrim khoảng trắng đầu/cuối trước khi lọc
doetCodestringTối đa 20 ký tựBắt buộc, lọc theo mã Sở GDĐT
divisionCodestring / nullTối đa 20 ký tựLọc theo mã phường/xã
schoolCodestring / nullTối đa 50 ký tựLọc theo mã trường
approvalStatusint / null0, 1, 20=Pending, 1=Approved, 2=Rejected
skipint / null>= 0Mặc định 0
takeint / null1..200Mặc định 20

Ví dụ gọi API:

curl --location --get 'https://lmsbpartnerapi.vstudy.edu.vn/api/lms-base/learning-resources' \
  --header 'X-Api-Key: <api-key-don-vi-duoc-cap>' \
  --data-urlencode 'approvalStatus=0' \
  --data-urlencode 'doetCode=01' \
  --data-urlencode 'skip=0' \
  --data-urlencode 'take=20'
bash

Ví dụ item response:

{
  "id": "01JQ2T5W11Z9J6AEX4Q6F90HME",
  "sourceId": "LR-HN-000001",
  "title": "Bài học: So sánh phân số",
  "status": "Rejected",
  "reason": "Nội dung chưa đúng chuẩn chương trình.",
  "occurredAt": "2026-04-12T07:12:44.0000000Z",
  "canDelete": false,
  "createdAt": "2026-04-12T07:12:44.0000000Z",
  "updatedAt": "2026-04-12T07:12:44.0000000Z"
}
json

Các trường trong item response:

FieldKiểu dữ liệuGhi chú
idstring (ULID)Mã định danh học liệu trên trục
sourceIdstringMã nguồn học liệu phía đối tác
titlestringTiêu đề học liệu
statusstringTrạng thái hiện tại: Pending, Approved, Rejected, Hidden, Removed. Trạng thái kiểm duyệt (Removed, Hidden) được ưu tiên trước trạng thái duyệt
reasonstring / nullLý do theo trạng thái hiện tại; chỉ có giá trị khi Rejected (lý do từ chối) hoặc Removed (lý do gỡ bỏ)
occurredAtstring (date-time) / nullThời điểm UTC trạng thái hiện tại được ghi nhận (fallback về thời điểm cập nhật/tạo nếu thiếu)
canDeletebooltrue khi học liệu đang Pending (được phép xóa)
createdAtstring (date-time)Thời điểm tạo UTC
updatedAtstring (date-time) / nullThời điểm cập nhật UTC

Lưu ý: query param approvalStatus chỉ lọc theo 3 trạng thái duyệt (0=Pending, 1=Approved, 2=Rejected); chưa hỗ trợ lọc trực tiếp theo Hidden/Removed. Trường status trong response vẫn phản ánh đầy đủ 5 trạng thái.

7.2. Endpoint danh sách học liệu tổ chức

  • Method: GET
  • Path: /api/lms-base/learning-resources/organizations

Query params (đều không bắt buộc):

FieldKiểu dữ liệuGiới hạnGhi chú
sourceIdstring / nullTối đa 128 ký tựLọc theo mã học liệu nguồn
educationLevelCodestring / nullTối đa 20 ký tựLọc theo mã cấp học
approvalStatusint / null0, 1, 20=Pending, 1=Approved, 2=Rejected
skipint / null>= 0Mặc định 0
takeint / null1..200Mặc định 20

Ví dụ gọi API:

curl --location --get 'https://lmsbpartnerapi.vstudy.edu.vn/api/lms-base/learning-resources/organizations' \
  --header 'X-Api-Key: <api-key-don-vi-duoc-cap>' \
  --data-urlencode 'approvalStatus=0' \
  --data-urlencode 'skip=0' \
  --data-urlencode 'take=20'
bash

7.3. Endpoint danh sách học liệu Sở

  • Method: GET
  • Path: /api/lms-base/learning-resources/doets
  • Bắt buộc truyền doetCode trong query.

Query params:

FieldKiểu dữ liệuGiới hạnGhi chú
sourceIdstring / nullTối đa 128 ký tựLọc theo mã học liệu nguồn
educationLevelCodestring / nullTối đa 20 ký tựLọc theo mã cấp học
doetCodestringTối đa 20 ký tựBắt buộc, lọc theo mã Sở GDĐT
divisionCodestring / null-Không hỗ trợ ở luồng Sở (nếu truyền sẽ bị từ chối)
schoolCodestring / null-Không hỗ trợ ở luồng Sở (nếu truyền sẽ bị từ chối)
approvalStatusint / null0, 1, 20=Pending, 1=Approved, 2=Rejected
skipint / null>= 0Mặc định 0
takeint / null1..200Mặc định 20

7.4. Endpoint danh sách học liệu xã/phường

  • Method: GET
  • Path: /api/lms-base/learning-resources/divisions
  • Bắt buộc truyền doetCodedivisionCode trong query.

Query params:

FieldKiểu dữ liệuGiới hạnGhi chú
sourceIdstring / nullTối đa 128 ký tựLọc theo mã học liệu nguồn
educationLevelCodestring / nullTối đa 20 ký tựLọc theo mã cấp học
doetCodestringTối đa 20 ký tựBắt buộc, lọc theo mã Sở GDĐT
divisionCodestringTối đa 20 ký tựBắt buộc, lọc theo mã đơn vị quản lý
schoolCodestring / null-Không hỗ trợ ở luồng xã/phường (nếu truyền sẽ bị từ chối)
approvalStatusint / null0, 1, 20=Pending, 1=Approved, 2=Rejected
skipint / null>= 0Mặc định 0
takeint / null1..200Mặc định 20

7.5. Cách theo dõi duyệt cho partner

Có 2 cơ chế theo dõi trạng thái duyệt:

  • Webhook (khuyến nghị): Trục LMS chủ động gọi tới endpoint của partner mỗi khi học liệu đổi trạng thái. Chi tiết ở mục 7.6.
  • Polling (dự phòng/đối soát): partner tự gọi API danh sách theo scope để tra cứu trạng thái.

Endpoint polling theo scope:

  • Nhà trường: GET /api/lms-base/learning-resources
  • Tổ chức: GET /api/lms-base/learning-resources/organizations
  • Sở: GET /api/lms-base/learning-resources/doets
  • Xã/phường: GET /api/lms-base/learning-resources/divisions

Lưu ý:

  • Khuyến nghị lọc theo ngữ cảnh của đơn vị và theo approvalStatus để giảm dữ liệu trả về.
  • Partner API hiện chưa có endpoint tra cứu trạng thái duyệt theo từng learningResourceId riêng biệt; tra cứu chủ động vẫn dùng API danh sách.
  • Field trạng thái trong item response danh sách (xem chi tiết ở mục 7.1):
    • status: trạng thái hiện tại dạng chuỗi, gồm Pending, Approved, Rejected, Hidden, Removed.
    • reason: lý do theo trạng thái hiện tại (có khi Rejected/Removed).
    • occurredAt: thời điểm trạng thái hiện tại được ghi nhận (UTC).

7.6. Webhook đồng bộ trạng thái học liệu

Trục LMS hỗ trợ webhook để chủ động thông báo cho partner khi trạng thái học liệu thay đổi, giúp partner đồng bộ gần thời gian thực mà không phải polling liên tục.

7.6.1. Đăng ký webhook

Quy trình đăng ký (cấu hình ở phía Trục LMS theo từng đơn vị, không tự đăng ký qua API):

  1. Đơn vị cần tích hợp webhook cung cấp cho QIG endpoint nhận sự kiện (WebhookUrl, khuyến nghị HTTPS).
  2. QIG cấp lại cho đơn vị khóa bí mật webhook (WebhookSecret) dùng để ký/xác minh HMAC.
  3. QIG bật webhook cho đơn vị sau khi có đủ thông tin.

Webhook chỉ được gửi khi đơn vị đang bật webhook và đã cấu hình đủ WebhookUrl + WebhookSecret.

Đầu mối liên hệ tích hợp webhook:

Đơn vịNgười phụ tráchSố điện thoạiEmail
QIGNguyễn Minh Tâm0354898828tamnv@qig.vn

7.6.2. Sự kiện và thời điểm gửi

  • eventType: learning-resource.status-changed.
  • Webhook được gửi khi trạng thái học liệu thay đổi sang một trong các giá trị sau:
statusÝ nghĩareason
ApprovedHọc liệu đã được duyệtnull
RejectedHọc liệu bị từ chối duyệtLý do từ chối
HiddenHọc liệu bị tạm ẩnnull
RemovedHọc liệu bị gỡ bỏLý do gỡ bỏ
  • Nếu trạng thái mới trùng trạng thái trước đó thì không gửi webhook.
  • Trạng thái Pending không phát webhook.
  • Nhiều học liệu của cùng một partner đổi trạng thái trong cùng một đợt xử lý sẽ được gom chung vào một event (mảng items).

7.6.3. Cấu trúc request gửi tới partner

  • Method: POST tới WebhookUrl của partner.
  • Content-Type: application/json.

Headers:

HeaderMô tả
X-Webhook-Event-IdMã định danh duy nhất của event (ULID). Dùng để chống xử lý trùng (idempotency)
X-Webhook-TimestampThời điểm phát sinh event, ISO-8601 UTC (ví dụ 2026-05-25T03:15:20.1234567Z). Là phần tham gia ký chữ ký
X-Webhook-SignatureChữ ký HMAC-SHA256, định dạng sha256=<hex chữ thường>
X-Webhook-SenderMã hệ thống gửi (mặc định base-lms)

Body mẫu (JSON, key dạng camelCase):

{
  "eventId": "01JW1234567890ABCDEFGHJKL",
  "eventType": "learning-resource.status-changed",
  "occurredAt": "2026-05-25T03:15:20.1234567Z",
  "items": [
    { "sourceId": "LR-HN-000001", "status": "Approved", "reason": null },
    { "sourceId": "LR-HN-000002", "status": "Rejected", "reason": "Nội dung chưa đúng chuẩn chương trình." }
  ]
}
json

Trường payload:

FieldKiểu dữ liệuMô tả
eventIdstringMã event (trùng header X-Webhook-Event-Id)
eventTypestringLuôn là learning-resource.status-changed
occurredAtstring (date-time)Thời điểm UTC phát sinh event (trùng header X-Webhook-Timestamp)
itemsarray<object>Danh sách học liệu đổi trạng thái trong event
items[].sourceIdstringMã nguồn học liệu phía partner đã gửi khi chia sẻ
items[].statusstringApproved | Rejected | Hidden | Removed
items[].reasonstring / nullLý do thay đổi trạng thái (thường có khi Rejected/Removed)

7.6.4. Xác minh chữ ký (bắt buộc)

Partner phải xác minh X-Webhook-Signature trước khi xử lý payload:

  1. Lấy timestamp = giá trị header X-Webhook-Timestamp.
  2. Lấy payload = nguyên văn body request (raw UTF-8, không serialize lại).
  3. Tính HMAC-SHA256 với khóa WebhookSecret trên chuỗi {timestamp}.{payload}.
  4. Encode kết quả sang hex chữ thường và thêm tiền tố sha256=.
  5. So sánh an toàn (constant-time) với header X-Webhook-Signature. Nếu khác → loại bỏ request.

Ví dụ tham khảo (Node.js):

const crypto = require("crypto");
 
function verifySignature(rawBody, timestamp, signatureHeader, webhookSecret) {
  const expected =
    "sha256=" +
    crypto.createHmac("sha256", webhookSecret).update(`${timestamp}.${rawBody}`).digest("hex");
 
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
js

7.6.5. Phản hồi và cơ chế retry

  • Partner trả HTTP 2xx để xác nhận đã nhận; nên phản hồi nhanh và xử lý nghiệp vụ bất đồng bộ.
  • Timeout mặc định mỗi lần gọi: 10 giây.
  • Trục retry tối đa 3 lần khi gặp lỗi tạm thời: HTTP 408, 429, 5xx, lỗi mạng hoặc timeout.
  • Các mã 4xx khác (trừ 408/429) được coi là lỗi vĩnh viễn và không retry.
  • Vì có retry, partner phải chống trùng theo eventId (xử lý idempotent).

7.6.6. Khuyến nghị cho partner

  • Dùng HTTPS cho WebhookUrl.
  • Lưu eventId đã xử lý để bỏ qua sự kiện trùng.
  • Vẫn nên có cron polling dự phòng (mục 7.5) để đối soát khi webhook lỗi kéo dài.
  • Bảo mật WebhookSecret; phối hợp đầu mối để xoay khóa khi cần.

8. Xóa học liệu pending

  • Method: DELETE
  • Path (scope trường): /api/lms-base/learning-resources
  • Path (scope tổ chức): /api/lms-base/learning-resources/organizations
  • Path (scope Sở): /api/lms-base/learning-resources/doets
  • Path (scope xã/phường): /api/lms-base/learning-resources/divisions
  • Content-Type: application/json

8.1. Dữ liệu đầu vào (request)

Request body là mảng string[] chứa danh sách id học liệu (ULID) cần xóa.

FieldKiểu dữ liệuBắt buộcRàng buộc
ids (request body)array<string>Mỗi phần tử là ULID học liệu; mảng không rỗng; không trùng id trong cùng request; tối đa 200 id/lần

8.2. Mô hình phản hồi (response model)

API trả về response envelope chuẩn:

  • success: true/false theo kết quả tổng thể của lô.
  • message: thông điệp tổng hợp (ví dụ: số lượng thành công/thất bại).
  • data: danh sách kết quả theo từng id đã gửi.

Chi tiết từng phần tử data[]:

FieldKiểu dữ liệuÝ nghĩa
sourceIdstringMã học liệu nguồn tương ứng bản ghi được xử lý
idstring (ULID)Mã ULID học liệu đã xử lý
deletedbooltrue: xóa thành công, false: không xóa được
messagestringLý do/diễn giải kết quả cho từng bản ghi
processedAtstring (datetime UTC)Thời điểm hệ thống xử lý bản ghi

Lưu ý: API có thể trả 200 OK nhưng vẫn có item deleted = false (thành công một phần), cần xử lý theo từng phần tử data[].

8.3. Ví dụ request

Ví dụ request:

[
  "01JQ2T5W11Z9J6AEX4Q6F90HME",
  "01JQ2T5W11Z9J6AEX4Q6F90HMF"
]
json

8.4. Ví dụ response (thành công một phần)

Ví dụ response:

{
  "success": false,
  "message": "Xử lý xóa 2 học liệu, xóa thành công 1, thất bại 1.",
  "data": [
    {
      "sourceId": "LR-HN-000001",
      "id": "01JQ2T5W11Z9J6AEX4Q6F90HME",
      "deleted": true,
      "message": "Xóa học liệu thành công.",
      "processedAt": "2026-04-14T02:00:00.0000000Z"
    },
    {
      "sourceId": "LR-HN-000002",
      "id": "01JQ2T5W11Z9J6AEX4Q6F90HMF",
      "deleted": false,
      "message": "Chỉ được xóa học liệu ở trạng thái chờ duyệt.",
      "processedAt": "2026-04-14T02:00:00.0000000Z"
    }
  ]
}
json

Ràng buộc xóa theo lô:

  • Chỉ xóa được học liệu ở trạng thái Pending (0).
  • Chỉ xóa được học liệu thuộc đúng đối tác gọi API (X-Api-Key).
  • Tối đa 200 id/lần.
  • Danh sách id không được rỗng và không được trùng trong cùng request.
  • API có thể trả 200 OK nhưng có bản ghi xóa thất bại; luôn kiểm tra data[].deleted.

9. Các rule validate dễ sai

  1. API tạo/chia sẻ học liệu chạy theo cơ chế upsert theo sourceId:
    • /learning-resources: key là schoolCode + sourceId trong phạm vi partner gọi API (X-Api-Key).
    • /learning-resources/organizations: key là partnerId + sourceId.
    • /learning-resources/doets: key là doetCode + sourceId trong phạm vi partner gọi API (X-Api-Key).
    • /learning-resources/divisions: key là doetCode + divisionCode + sourceId trong phạm vi partner gọi API (X-Api-Key).
    • Nếu bản ghi hiện tại đã Approved thì không được cập nhật đè.
  2. authorPinCode/creatorPinCode ở endpoint /learning-resources phải là CCCD 12 số tồn tại trong hệ thống Sở theo đúng ngữ cảnh dữ liệu gửi lên.
  3. gradeCode/subjectCode/knowledgeUnitCode không dùng mã tự đặt; phải map từ danh mục dùng chung.
  4. sourceId, title, authorName, creatorName, contents[].name chỉ chấp nhận văn bản thuần, không chứa HTML.
  5. description học liệu (nếu truyền) cho phép văn bản hoặc HTML an toàn, tối đa 1024 ký tự.
  6. Khi tạo mới/cập nhật hợp lệ, hệ thống luôn gán trạng thái mặc định: ApprovalStatus = Pending (0)ModerationStatus = Normal.
  7. thumbnailUrl là bắt buộc và phải là URL tuyệt đối hợp lệ. Có thể dùng path ảnh đã upload hoặc URL ảnh ngoài đúng định dạng ảnh hỗ trợ.
  8. contents là bắt buộc và phải có ít nhất 1 phần tử.
  9. SCORM phải là URL tới .zip đã upload, đúng MIME application/zip, và đúng chuẩn SCORM hỗ trợ.
  10. YOUTUBE phải là liên kết YouTube tuyệt đối hợp lệ (không truyền iframe HTML).
  11. IFRAME phải là URL tuyệt đối (http/https) có domain/subdomain thuộc danh sách cho phép (mặc định youtube.com, olm.vn, sgk.olm.vn); không truyền thẻ <iframe> HTML.
  12. fileIds nếu truyền thì tất cả phải thuộc partner và phải được dùng thực tế trong thumbnailUrl hoặc contents.
  13. URL trong TEXT HTML (src/href/poster) bắt buộc là URL tuyệt đối hợp lệ (http/https).
  14. HTML không được chứa nội dung không an toàn (CSS/JS/iframe/object/embed/on*/protocol cấm).
  15. semesterCode chỉ nhận 1 hoặc 2; yearCode phải là năm 4 chữ số hợp lệ và tồn tại trong danh mục năm học hệ thống.
  16. API danh sách:
    • Nhà trường: bắt buộc doetCode.
    • Tổ chức: không yêu cầu doetCode.
    • Sở: bắt buộc doetCode, không cho truyền divisionCode/schoolCode.
    • Xã/phường: bắt buộc doetCode + divisionCode, không cho truyền schoolCode.
    • Tất cả endpoint danh sách đều yêu cầu take trong khoảng 1..200skip >= 0.

10. Các lỗi thường gặp và cách xử lý

10.1. Theo HTTP status

HTTP statusNguyên nhân thường gặpCách xử lý
401 UnauthorizedThiếu/sai X-Api-KeyKiểm tra key và header
400 Bad RequestSai format dữ liệu, ULID, mã danh mục, rule mapping nội dung/fileĐối chiếu payload theo checklist và rule validate
404 Not FoundKhông tìm thấy attachmentIdKiểm tra lại id file và ngữ cảnh partner
200 OK (API xóa lô)Có thể thành công một phầnLuôn đọc data[].deleteddata[].message

10.2. Theo message nghiệp vụ thường gặp

Message lỗiNguyên nhânCách xử lý
Mã sở giáo dục là bắt buộc.Gọi API danh sách nhà trường/Sở/xã-phường nhưng thiếu doetCodeBổ sung doetCode khi gọi GET /learning-resources, GET /learning-resources/doets hoặc GET /learning-resources/divisions; scope tổ chức dùng endpoint riêng và không yêu cầu trường này
Không tìm thấy file cho các FileId: ...fileIds không tồn tại hoặc sai idKiểm tra lại id upload trả về
FileId ... không thuộc đối tác gọi API.Dùng file của partner khácChỉ dùng attachment thuộc cùng X-Api-Key
FileId ... đã được dùng bởi đối tượng khác.File đã bị gán owner khácUpload file mới hoặc dùng đúng file đang thuộc học liệu cần thao tác
Các FileId đã upload nhưng chưa được replace vào nội dung: ...Truyền fileIds nhưng chưa dùng trong thumbnailUrl/contentsReplace đúng URL trước khi gọi tạo học liệu
Đường dẫn ảnh đại diện là bắt buộc.Thiếu thumbnailUrlBổ sung thumbnailUrl hợp lệ trước khi gọi API
Đường dẫn ảnh đại diện phải là URL tuyệt đối.thumbnailUrl không phải URL tuyệt đốiChuẩn hóa thành URL tuyệt đối trước khi gửi
Đường dẫn ảnh đại diện phải là URL tuyệt đối hợp lệ.thumbnailUrl không phải URL tuyệt đối http/httpsChuẩn hóa thành URL tuyệt đối hợp lệ
Đường dẫn ảnh đại diện (URL ngoài) phải có định dạng ảnh hợp lệ (.jpg, .jpeg, .png, .gif, .bmp, .webp).thumbnailUrl là URL ngoài nhưng không đúng định dạng ảnh hỗ trợDùng URL ảnh ngoài đúng định dạng hỗ trợ hoặc dùng ảnh đã upload lên trục
Mã học liệu nguồn chỉ chấp nhận văn bản thuần, không chứa HTML.Truyền HTML vào sourceIdChỉ gửi văn bản thuần cho mã nguồn học liệu
Tên học liệu chỉ chấp nhận văn bản thuần, không chứa HTML.Truyền HTML vào titleChỉ gửi văn bản thuần cho trường metadata
Tên tác giả chỉ chấp nhận văn bản thuần, không chứa HTML.Truyền HTML vào authorName hoặc creatorNameChỉ gửi văn bản thuần cho tên tác giả/người chia sẻ
Tên nội dung học liệu chỉ chấp nhận văn bản thuần, không chứa HTML.Truyền HTML vào contents[].nameChỉ gửi văn bản thuần cho tên nội dung
Học liệu phải có ít nhất 1 nội dung.Mảng contents rỗngBổ sung ít nhất 1 phần tử nội dung hợp lệ
SCORM content phải replace bằng path file package .zip đã upload lên trục.SCORM không trỏ vào file .zip đã uploadUpload .zip đúng chuẩn rồi replace URL
Content với type YOUTUBE chỉ chấp nhận dạng https://youtube.com/watch?v=... (bao gồm www.youtube.com/watch?v=...) hoặc https://youtu.be/...YOUTUBE không đúng 1 trong 2 pattern hỗ trợ hoặc đang gửi iframeChỉ gửi URL tuyệt đối đúng pattern, không gửi HTML iframe
Content với type IFRAME chỉ chấp nhận URL thuộc các domain được phép: ...IFRAME trỏ tới domain không nằm trong danh sách cho phép, không phải URL tuyệt đối http/https, hoặc đang gửi thẻ <iframe>Chỉ gửi URL tuyệt đối thuộc domain/subdomain được phép (mặc định youtube.com, olm.vn, sgk.olm.vn)
Content type '...' không phù hợp định dạng file '...'.type không khớp MIME file thực tếChọn type đúng với MIME đã upload
Content không được chứa CSS/TypeScript/JavaScript, iframe hoặc mã không an toàn.HTML vi phạm rule an toànLoại bỏ thành phần cấm trước khi gửi
Học liệu SourceId '...' đã được duyệt. Không cho phép cập nhật, vui lòng tạo học liệu mới để chia sẻ.Upsert gặp bản ghi cùng key đang ở trạng thái ApprovedDùng sourceId mới để tạo bản ghi chia sẻ mới
Không tìm thấy học liệu để cập nhật.Bản ghi upsert vừa đổi trạng thái hoặc không còn tồn tại tại thời điểm cập nhậtGọi lại API với payload mới nhất
Học liệu SourceId '...' đã được duyệt hoặc không còn ở trạng thái cho phép cập nhật.Bản ghi không còn ở trạng thái Pending/Rejected khi xử lý cập nhậtTạo bản ghi mới với sourceId khác
Không thể tải tệp lên hệ thống.File vi phạm validate extension/size/MIME hoặc lỗi upload storageKiểm tra extension, kích thước, MIME thực tế và thử upload lại

11. FAQ cho partner

1) Có bắt buộc truyền fileIds không?

  • Không bắt buộc.
  • Nhưng nếu truyền thì tất cả id phải hợp lệ, thuộc partner, và được dùng thực tế trong thumbnailUrl hoặc contents.

2) authorPinCodecreatorPinCode có thể giống nhau không?

  • Có thể giống hoặc khác.
  • Với endpoint /learning-resources, cả hai phải là CCCD 12 số hợp lệ và tồn tại trong ngữ cảnh dữ liệu Sở.
  • Với endpoint /learning-resources/organizations, có thể để trống để hệ thống tự điền mặc định theo thông tin tổ chức.
  • Với endpoint /learning-resources/doets, hệ thống tự gán theo mã/tên Sở.
  • Với endpoint /learning-resources/divisions, hệ thống tự gán theo mã/tên xã-phường.

3) Có thể gửi TEXT với HTML không?

  • Có.
  • Nhưng URL trong src/href/poster phải là URL tuyệt đối và nội dung phải qua rule an toàn HTML.

4) Có webhook/callback khi duyệt học liệu không?

  • Có. Trục LMS chủ động gửi webhook learning-resource.status-changed tới WebhookUrl của partner khi học liệu đổi trạng thái sang Approved, Rejected, Hidden hoặc Removed. Chi tiết payload, headers, xác minh chữ ký và retry ở mục 7.6.
  • Webhook cần được đầu mối cấp X-Api-Key cấu hình trước (WebhookUrl + WebhookSecret + bật webhook).
  • Có thể kết hợp polling API danh sách theo scope để đối soát/dự phòng khi webhook lỗi kéo dài:
    • Nhà trường: GET /learning-resources
    • Tổ chức: GET /learning-resources/organizations
    • Sở: GET /learning-resources/doets
    • Xã/phường: GET /learning-resources/divisions

5) Khi nào được xóa học liệu?

  • Chỉ xóa được học liệu ở trạng thái Pending (0) và thuộc đúng partner.

6) Tôi bị lỗi trùng sourceId, xử lý thế nào?

  • Nếu bản ghi cùng key đang Approved, cần dùng sourceId mới để chia sẻ bản ghi mới.
  • Nếu bản ghi cùng key đang Pending hoặc Rejected, API sẽ cập nhật bản ghi cũ và trả operation = updated.
  • Khuyến nghị lưu mapping sourceId ở hệ thống nguồn để kiểm soát luồng upsert.

11.1. Quy ước vận hành đã chốt

  • Scope upsert của sourceId:
    • /learning-resources: schoolCode + sourceId trong phạm vi X-Api-Key
    • /learning-resources/organizations: partnerId + sourceId
    • /learning-resources/doets: doetCode + sourceId trong phạm vi X-Api-Key
    • /learning-resources/divisions: doetCode + divisionCode + sourceId trong phạm vi X-Api-Key
  • Cơ chế dọn file upload không được mapping: hệ thống trục có job tự động quét dọn.
  • Trạng thái duyệt: ưu tiên webhook learning-resource.status-changed (mục 7.6); polling API danh sách dùng để đối soát/dự phòng.