API Ngân hàng đề

Tài liệu

Tài liệu API

Những gì cần để tích hợp ngân hàng câu hỏi vào hệ thống của trung tâm: xác thực, lấy câu theo ma trận, xem lại lượt gọi và dựng PDF.

API trả câu hỏi THPT (Toán, Vật lí, Hóa học) sinh từ ngân hàng khuôn: chọn chủ đề, loại câu, mức độ và số câu, nhận về đề bài, phương án, đáp án và lời giải. Công thức viết bằng LaTeX trong cặp $…$; hình vẽ là SVG dựng sẵn ở trường figure.

Gốc của mọi endpoint: https://cauhoi.vietify.vn/v1. Body gửi và nhận đều là JSON (UTF-8), tối đa 256KB.

Mỗi lượt ra câu thành công là một đề: hiện ở mục Đề đã tạo trên panel (xem câu hỏi, đáp án, in PDF) y như đề tạo tay trên web, và không bao giờ bị xoá. Log của từng lượt gọi (yêu cầu, kết quả JSON) thì giữ 30 ngày rồi xoá hẳn.

Xác thực

Tạo API key ở trang API keys trong panel (cần xác minh email trước). Gửi key qua header X-API-Key, hoặc Authorization: Bearer <key>. Key chỉ hiện một lần lúc tạo; mất thì thu hồi rồi tạo key mới.

curl https://cauhoi.vietify.vn/v1/subjects \
  -H "X-API-Key: $API_KEY"

Hạn mức

  • Mặc định 60 lượt gọi mỗi phút cho một tài khoản — tính chung mọi key của tài khoản ấy.
  • Dựng PDF: 10 lượt mỗi phút.
  • Tối đa 500 câu mỗi lượt lấy câu hoặc dựng lại câu; tối đa 200 câu mỗi loại.
  • Vượt hạn mức nhận 429 kèm header Retry-After (số giây). Mỗi lời đáp có X-RateLimit-Limit và X-RateLimit-Remaining.

Mức độ và loại câu

Mức độ luôn là số. Trong khoá của object levels thì viết dạng chuỗi số ("1") vì JSON chỉ cho khoá là chuỗi; ở mọi chỗ khác là số nguyên (3).

MứcTên
1Nhận biết
2Thông hiểu
3Vận dụng
4Vận dụng cao
Loại câuTênNội dung trả về
single_choiceTrắc nghiệm nhiều phương ánoptions[]: {text, correct}; thứ tự phương án là A, B, C, D
true_falseTrắc nghiệm đúng – saistatements[]: {text, correct, solution}
short_answerTrả lời ngắnanswer: {text, value, digits}
essayTự luậnparts[] (các ý), answer: {text}

Mọi câu đều kèm solution (lời giải, các bước cách nhau bằng xuống dòng) và price (giá của chính câu ấy). Câu có hình kèm figure: {format, svg, width, height, scene}.

Lỗi

Mọi lỗi cùng một dạng. code để máy đọc, message để người đọc; lỗi theo từng trường nằm ở errors. Không lỗi nào bị tính tiền. Lỗi của lượt lấy câu kèm header X-Request-Id để tra lại.

{
  "error": {
    "code": "validation_failed",
    "message": "Yêu cầu có trường không hợp lệ.",
    "errors": {
      "topics.0": ["`g12.integrals` là mã chương — lấy cả chương thì viết `g12.integrals.*`."],
      "types.single_choice.levels.NB": ["Khoá mức là số: 1 Nhận biết, 2 Thông hiểu, 3 Vận dụng, 4 Vận dụng cao. Viết \"1\" thay cho \"NB\"."]
    }
  }
}
HTTPcodeKhi nào
400invalid_jsonBody không phải JSON hợp lệ
401invalid_keyThiếu key, key sai hoặc đã thu hồi
402insufficient_balanceSố dư khả dụng không đủ số tiền cần giữ (hoặc phí PDF)
403account_locked, email_unverifiedTài khoản bị khoá hoặc chưa xác minh email
404request_not_found, exam_not_found, not_foundKhông có lượt gọi hay đề này (hoặc của tài khoản khác); không có endpoint
409not_enough_questionsKho không gom đủ câu trong phạm vi; details.types nói loại nào xin bao nhiêu, có bao nhiêu
409no_questions, pdf_in_progressDựng PDF cho lượt không thành công (/v1/requests/{id}/pdf; gọi theo mã đề thì 404); PDF cùng payload đang dựng (có Retry-After)
409not_original_exam, variants_in_progressTạo đề phụ từ một đề phụ (details.parent_id là đề gốc của nó); đề gốc đang tạo một loạt đề phụ khác hay đang đổi câu (có Retry-After)
409exam_changed, matrix_outdatedĐề vừa được đổi câu trong lúc dựng PDF (in lại để có bản mới); mọi chủ đề trong ma trận của đề đã rời danh mục (đề phụ đổi câu hỏi)
410log_expiredLog của lượt gọi đã quá 30 ngày lưu (GET /v1/requests/{id}); đề của lượt ấy vẫn còn
405method_not_allowedEndpoint không nhận phương thức HTTP ấy
413payload_too_largeBody lớn hơn 256KB
422validation_failedTrường sai; lời báo chỉ đúng cách viết đúng
429rate_limitedVượt hạn mức; chờ Retry-After giây
503bank_unavailable, pdf_build_failedKho câu hỏi bận hay lỗi; dựng PDF hỏng. Gọi lại sau Retry-After giây
500internal_errorLỗi không lường trước; tiền giữ của lượt ấy đã nhả, không bị tính tiền. Gọi lại được. Riêng PDF, nếu lời báo nói đã trừ phí thì gọi lại đúng payload để lấy file, không thu thêm
503maintenanceHệ thống đang cập nhật, thường chỉ vài phút. Gọi lại sau Retry-After giây

GET /v1/subjects

Cây môn → lớp → chương → chủ đề. Mỗi chủ đề kèm số khuôn, các loại câu và các mức độ kho ra được (type_levels nói rõ từng loại ra được mức nào). Miễn phí. Mã lớp (g12), mã chương, mã chủ đề lấy ở đây là thứ truyền vào topics, exclude, limits.

GET /v1/me

Số dư (balance), tiền đang giữ cho lượt gọi đang chạy (held), khả dụng (available), hạn mức và bảng giá hiện hành. Miễn phí.

GET /v1/brands

Thương hiệu in PDF của tài khoản — tạo, sửa ở panel → Thương hiệu: brand_id, tên, các ô bộ mặt tờ đề (unit, header, header_right, footer, student_fields, style, font, color). Gửi "brand": "<brand_id>" khi in PDF thay cho các ô ấy. Miễn phí.

POST /v1/questions

Lấy câu hỏi theo phạm vi chủ đề và ma trận loại × mức. Mỗi lần gọi đều tính tiền theo từng câu nhận được, kể cả gọi lại đúng yêu cầu cũ; lấy lại kết quả cũ thì dùng GET /v1/requests/{id}.

curl -X POST https://cauhoi.vietify.vn/v1/questions \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "subject": "math",
      "seed": "20260924",
      "topics": [
          "g12.*"
      ],
      "exclude": [
          "g12.integrals.area-under-curve"
      ],
      "types": {
          "single_choice": {
              "count": 12,
              "levels": {
                  "1": 4,
                  "2": 4,
                  "3": 3,
                  "4": 1
              }
          },
          "true_false": {
              "count": 4
          },
          "short_answer": {
              "count": 6
          }
      }
  }'
TrườngBắt buộcQuy tắc
subjectcóMã môn trong danh mục: math, physics, chemistry
typescóKhoá là loại câu. Mỗi loại có count (0–200) và/hoặc levels (khoá "1"–"4"). Có cả hai thì count phải bằng tổng levels. Tổng mọi loại từ 1 tới 500. Số câu mỗi mức có thể lệch ±1 khi kho thiếu mức ấy — warnings nói rõ
topicskhôngMã chủ đề nguyên văn, <chương>.* hoặc <lớp>.*. Rỗng là cả môn. Mã chương thiếu .* hay mã chủ đề kèm .* đều bị từ chối
excludekhôngNhư topics, trừ bớt khỏi phạm vi
limitskhôngSố câu tối đa mỗi phạm vi góp: khoá là mã lớp, chương hoặc chủ đề, không kèm .*
personalizationkhôngMã chương, chủ đề học sinh đang yếu — đề dồn nhiều câu vào đó. Phải nằm trong phạm vi topics
exceptskhôngCâu đã lấy ở đề trước, lần này loại ra: {id, type?, level?}. Tối đa 1000 mục
seedkhôngChuỗi chữ số. Cùng seed và cùng yêu cầu thì ra đúng một đề. Bỏ trống thì hệ thống tự bốc và trả lại
namekhôngTên đề, chuỗi 1–150 ký tự, để nhận ra đề ở mục Đề đã tạo. Không gửi sang kho câu hỏi. Bỏ trống thì tự đặt theo môn và phạm vi

Kết quả luôn kèm đáp án và lời giải; không nhận trường include_answer. Trường lạ bị từ chối — gõ nhầm topic thay cho topics là biết ngay; tham số trên URL cũng vậy (mọi trường nằm trong body JSON). Header lời đáp: X-Request-Id, X-Cost (tiền lượt này), X-Balance (khả dụng còn lại).

{
  "request_id": "01926a6e-3b1f-7c2d-8e4f-5a6b7c8d9e0f",
  "seed": "20260924",
  "count": 22,
  "cost": 790,
  "balance": 124210,
  "warnings": [],
  "sections": [
    {
      "type": "single_choice",
      "count": 12,
      "questions": [
        {
          "id": "g12.applications-of-derivatives.variation-table.reading-variation-table",
          "seed": "7002024519329879594",
          "type": "single_choice",
          "topic": "g12.applications-of-derivatives.variation-table",
          "skill": "variation-table/read-extrema-and-root-count",
          "level": 3,
          "version": 3,
          "price": 30,
          "stem": "Cho hàm số $y = f(x)$ …",
          "options": [
            {"text": "$6$", "correct": false},
            {"text": "$4$", "correct": true},
            {"text": "$5$", "correct": false},
            {"text": "$3$", "correct": false}
          ],
          "solution": "Đường thẳng $y = m$ …"
        }
      ]
    }
  ]
}

POST /v1/questions/re-render

Dựng lại câu từ bộ (id, seed, type, level) đã lưu — ra đúng nguyên văn câu cũ. Đổi level là ra câu khác của cùng khuôn ở mức ấy. Tính tiền như lấy câu; câu nào không dựng được nằm trong warnings và không tính tiền. Mỗi lượt dựng lại thành công là một đề mới; nhận thêm trường name như lấy câu.

{
    "questions": [
        {
            "id": "g12.integrals.computing-integrals.monomial-integral",
            "seed": "13677815959902261144",
            "type": "single_choice",
            "level": 3
        }
    ]
}

GET /v1/requests/{id}

Xem lại log một lượt gọi của tài khoản: yêu cầu đã gửi (request), bảng giá lúc ấy (prices), số tiền (cost) và đúng kết quả hay lỗi đã nhận (response). Miễn phí, không gọi lại kho. Log giữ 30 ngày rồi xoá hẳn: lượt thành công quá hạn nhận 410 (đề của lượt ấy vẫn còn), lượt không thành quá hạn nhận 404.

GET /v1/exams/{id}

Cả một đề: đề gốc (mã của nó là request_id của lượt ra câu) hay đề phụ (mã exam_id nhận lúc tạo đề phụ). Câu hỏi đủ đáp án, lời giải như lúc nhận; code là mã đề; đề phụ có parent_id, number và cách tạo ở variant. Đề gốc kèm danh sách đề phụ của nó (variants, chỉ tóm tắt). Miễn phí, không gọi lại kho; đề không bao giờ bị xoá nên gọi được bất cứ lúc nào, kể cả khi log đã dọn. Câu của đề phụ không kèm price: đề phụ tính tiền cả đề, không theo từng câu. revision tăng mỗi lần khách đổi một câu trên trang đề (đề phụ xáo, đổi bộ số đổi theo đề gốc); PDF in trước đó là bản cũ, in lại là PDF mới.

{
  "exam_id": "01926a6e-3b1f-7c2d-8e4f-5a6b7c8d9e0f",
  "parent_id": null,
  "number": null,
  "name": "Kiểm tra 45 phút · Ứng dụng đạo hàm",
  "code": "101",
  "kind": "questions",
  "variant": null,
  "source": "api",
  "created_at": "2026-09-25T09:15:02+07:00",
  "count": 22,
  "cost": 790,
  "revision": 1,
  "seed": "20260924",
  "variants": [
    {"exam_id": "01926a71-8d0a-…", "number": 1, "name": "Đề phụ 1", "code": "102", "variant": {…}, "count": 22, "cost": 237, "created_at": "…"}
  ],
  "warnings": [],
  "sections": [ … ]
}

POST /v1/exams/{id}/variants

Tạo đề phụ — các mã đề phát cho học sinh — từ một đề gốc. Mỗi đề phụ là một đề riêng, không có tên riêng mà gọi theo số: Đề phụ 1, Đề phụ 2…, mang mã đề nối tiếp mã của họ đề (đề gốc chưa có mã thì nhận 101 cùng loạt đề phụ đầu tiên; đề phụ 102, 103…; mã dạng số giữ đúng độ dài, 001 thì tiếp 002). Lời đáp chỉ có tóm tắt từng đề phụ — mười đề đủ câu hỏi trong một lời đáp là quá nặng; lấy câu hỏi từng đề bằng GET /v1/exams/{id}, in bằng POST /v1/exams/{id}/pdf.

curl -X POST https://cauhoi.vietify.vn/v1/exams/$EXAM_ID/variants \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
      "count": 3,
      "shuffle_questions": true,
      "shuffle_options": true,
      "reseed": true
  }'
TrườngBắt buộcQuy tắc
countcóSố đề phụ, từ 1. Tối đa mỗi lần: 50 khi chỉ xáo, 20 khi đổi bộ số, 10 khi đổi câu hỏi; một đề gốc có tối đa 99 đề phụ
shuffle_questionskhôngtrue: xáo thứ tự câu trong từng phần — phần I, II, III giữ nguyên thứ tự
shuffle_optionskhôngtrue: xáo A, B, C, D của câu nhiều phương án, correct đi theo phương án. Ý a, b, c, d của câu đúng sai giữ nguyên (lời giải gắn với chữ của ý)
reseedkhôngtrue: đổi bộ số — cùng câu, cùng loại, cùng mức, số liệu mới nên đáp án khác đề gốc. Kéo theo shuffle_options: số liệu mới thì đáp án đúng cũng đổi chỗ
new_questionskhôngtrue: đổi câu hỏi — rút câu khác từ đúng yêu cầu của đề gốc (phạm vi, loại, mức, số câu). Đề mới hoàn toàn: ba cách kia coi như có sẵn, không xáo thêm. Không dùng được với đề dựng lại câu
codeskhôngMã đề tự đặt: mảng đúng count chuỗi, mỗi mã tối đa 10 ký tự, không trùng nhau và không trùng mã đã có trong họ đề. Bỏ trống thì tự đánh

Phải bật ít nhất một cách; bật nhiều cách được (đổi bộ số rồi xáo luôn thứ tự câu). Cách nặng bao luôn cách nhẹ — variant của đề phụ ghi đúng như thế — và cách nặng nhất quyết định giá:

Cách tạoGiá mỗi đề phụ
Chỉ xáo câu, xáo phương án10% giá đề gốc, làm tròn lên. Giá đề gốc là tiền các câu đang có trong đề, không kể PDF: chưa đổi câu nào thì bằng cost. Không gọi kho câu hỏi
Đổi bộ số (có xáo hay không)30% giá đề gốc, làm tròn lên. Câu nào kho chưa dựng được bộ số mới thì giữ số liệu của đề gốc và nói rõ ở warnings
Đổi câu hỏiNhư một lượt lấy câu: theo giá từng câu nhận được. Hỏng giữa loạt thì giữ các đề phụ đã tạo, phần còn lại không tính tiền, warnings nói rõ

PDF của đề phụ tính theo trang như mọi đề. Mỗi lần gọi để lại một log (endpoint variants) xem được ở Logs và bằng GET /v1/requests/{request_id}; tạo đề phụ trên panel thì không để log.

{
  "request_id": "01926a71-8c2e-7a41-9b5d-3e4f5a6b7c8d",
  "exam_id": "01926a6e-3b1f-7c2d-8e4f-5a6b7c8d9e0f",
  "count": 3,
  "cost": 711,
  "balance": 123499,
  "warnings": [],
  "variants": [
    {
      "exam_id": "01926a71-8d0a-7b12-a3c4-5d6e7f8a9b0c",
      "number": 1,
      "name": "Đề phụ 1",
      "code": "102",
      "variant": {"shuffle_questions": true, "shuffle_options": true, "reseed": true, "new_questions": false},
      "count": 22,
      "cost": 237,
      "created_at": "2026-09-25T10:02:11+07:00"
    }
  ]
}

POST /v1/exams/{id}/pdf

Dựng PDF của một đề — đề gốc hay đề phụ — và trả thẳng file application/pdf. Với đề gốc, POST /v1/requests/{request_id}/pdf là cùng một endpoint. Đề không bao giờ bị xoá nên in được bất cứ lúc nào, kể cả khi log của lượt ấy đã bị dọn; PDF in qua API cũng hiện trong trang đề trên panel. Phí = số trang × đơn giá trang; header X-Pages, X-Cost, X-Balance, X-Exam-Id (đề đã in), và X-Render-Id — mã của lần gọi này, có cả khi lỗi, để tra lại ở panel → Logs → PDF.

curl -X POST https://cauhoi.vietify.vn/v1/exams/$EXAM_ID/pdf \
  -H "X-API-Key: $API_KEY" \
  -d '{
      "mode": "exam",
      "unit": [
          "SỞ GD&ĐT HÀ NỘI",
          "TRUNG TÂM ABC"
      ],
      "exam_name": "Kỳ thi thử tốt nghiệp THPT năm 2026",
      "duration": 90,
      "exam_code": "101",
      "header": "Trung tâm <b>ABC</b> — Đề luyện tập số 1",
      "header_right": "Năm học 2025–2026",
      "footer": "Xem đáp án tại <a href=\"https://abc.vn\">abc.vn</a>",
      "style": "modern",
      "font": "be-vietnam-pro",
      "color": "#0f766e"
  }' \
  -o de-thi.pdf
TrườngBắt buộcQuy tắc
modecóexam — tờ đề, không đáp án; answers — bảng đáp án và lời giải chi tiết
brandkhôngId thương hiệu (GET /v1/brands): thay cho unit, header, header_right, footer, student_fields, style, font, color — gửi brand thì không gửi kèm các ô ấy
unitcó (trừ khi gửi brand)Tên đơn vị, mảng 1–3 dòng, mỗi dòng tối đa 80 ký tự
exam_namecóTên kỳ thi, tối đa 150 ký tự
durationcóThời gian làm bài, số nguyên phút 1–600
subject_namekhôngTên môn in trên đề; bỏ trống thì lấy tên môn trong danh mục
exam_codekhôngMã đề, tối đa 10 ký tự
header, header_rightkhôngĐầu mỗi trang, bên trái và bên phải; tối đa 300 ký tự, nhận chút HTML (dưới bảng)
footerkhôngChân mỗi trang, bên trái; như header. Bên phải luôn là "Mã đề 101 – Trang 2/4" (không có mã đề thì "Trang 2/4")
student_fieldskhôngIn dòng "Họ và tên / Số báo danh"; mặc định true
stylekhôngKiểu tờ đề: classic — mẫu đề thi quen thuộc, hai cột tiêu đề (mặc định); modern — dải màu mang tên đơn vị, nhãn phần nền màu; minimal — một màu, nét kẻ mảnh
fontkhônglibertinus, times, noto-serif hoặc be-vietnam-pro. Bỏ trống thì theo kiểu: classic — libertinus, modern — be-vietnam-pro, minimal — noto-serif
colorkhôngMàu nhấn của tên phần, số câu, chữ cái phương án: dấu # và 6 chữ số hex viết thường, ví dụ "#b42318". Bỏ trống thì theo kiểu: #1d4ed8, #0f766e, #111111

Đầu, chân trang nhận chút HTML để gắn link, nhấn chữ: <a href="…"> (https://, http://, mailto:), <b>, <i>, <u>, <br>; đường dẫn https:// viết trần cũng thành link bấm được trong PDF. Thẻ khác, thuộc tính khác hay thẻ chưa đóng nhận 422 kèm cách viết đúng.

Nội dung câu hỏi và công thức luôn in màu đen, nên tờ đề photo đen trắng vẫn rõ. Ghi rõ giá trị mặc định (ví dụ "style": "classic") hay bỏ trống là cùng một payload.

In bằng brand là chép các ô của thương hiệu vào payload ngay lúc gọi: PDF đã in giữ nguyên bộ mặt lúc ấy, kể cả khi thương hiệu bị sửa hay xoá sau đó. Thương hiệu chưa sửa thì in lại không mất thêm; sửa ô nào thì in lại là PDF mới.

Cùng đề và cùng payload chỉ thu phí một lần: gọi lại trả đúng file cũ; file hết hạn giữ (10 phút) thì dựng lại không thu phí. Đổi bất kỳ trường nào, kể cả mode, là một PDF mới.

Seed và JavaScript

Seed của từng câu là số nguyên 64 bit không dấu — gần như luôn lớn hơn Number.MAX_SAFE_INTEGER của JavaScript. API luôn trả seed dạng chuỗi và chỉ nhận seed dạng chuỗi. Giữ nguyên chuỗi ấy khi lưu và khi gửi lại; đổi sang số là seed bị làm tròn và dựng lại ra câu khác.