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
429kèm headerRetry-After(số giây). Mỗi lời đáp cóX-RateLimit-Limitvà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ức | Tên |
|---|---|
1 | Nhận biết |
2 | Thông hiểu |
3 | Vận dụng |
4 | Vận dụng cao |
| Loại câu | Tên | Nội dung trả về |
|---|---|---|
single_choice | Trắc nghiệm nhiều phương án | options[]: {text, correct}; thứ tự phương án là A, B, C, D |
true_false | Trắc nghiệm đúng – sai | statements[]: {text, correct, solution} |
short_answer | Trả lời ngắn | answer: {text, value, digits} |
essay | Tự luận | parts[] (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\"."]
}
}
}
| HTTP | code | Khi nào |
|---|---|---|
| 400 | invalid_json | Body không phải JSON hợp lệ |
| 401 | invalid_key | Thiếu key, key sai hoặc đã thu hồi |
| 402 | insufficient_balance | Số dư khả dụng không đủ số tiền cần giữ (hoặc phí PDF) |
| 403 | account_locked, email_unverified | Tài khoản bị khoá hoặc chưa xác minh email |
| 404 | request_not_found, exam_not_found, not_found | Không có lượt gọi hay đề này (hoặc của tài khoản khác); không có endpoint |
| 409 | not_enough_questions | Kho 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 |
| 409 | no_questions, pdf_in_progress | Dự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) |
| 409 | not_original_exam, variants_in_progress | Tạ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) |
| 409 | exam_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) |
| 410 | log_expired | Log 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 |
| 405 | method_not_allowed | Endpoint không nhận phương thức HTTP ấy |
| 413 | payload_too_large | Body lớn hơn 256KB |
| 422 | validation_failed | Trường sai; lời báo chỉ đúng cách viết đúng |
| 429 | rate_limited | Vượt hạn mức; chờ Retry-After giây |
| 503 | bank_unavailable, pdf_build_failed | Kho câu hỏi bận hay lỗi; dựng PDF hỏng. Gọi lại sau Retry-After giây |
| 500 | internal_error | Lỗ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 |
| 503 | maintenance | Hệ 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ường | Bắt buộc | Quy tắc |
|---|---|---|
subject | có | Mã môn trong danh mục: math, physics, chemistry |
types | có | 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õ |
topics | không | Mã 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 |
exclude | không | Như topics, trừ bớt khỏi phạm vi |
limits | không | Số 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 .* |
personalization | không | Mã 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 |
excepts | không | Câu đã lấy ở đề trước, lần này loại ra: {id, type?, level?}. Tối đa 1000 mục |
seed | không | Chuỗ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 |
name | không | Tê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ường | Bắt buộc | Quy tắc |
|---|---|---|
count | có | 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_questions | không | true: xáo thứ tự câu trong từng phần — phần I, II, III giữ nguyên thứ tự |
shuffle_options | không | true: 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 ý) |
reseed | không | true: đổ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_questions | không | true: đổ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 |
codes | không | Mã đề 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ạo | Giá mỗi đề phụ |
|---|---|
| Chỉ xáo câu, xáo phương án | 10% 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ỏi | Như 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ường | Bắt buộc | Quy tắc |
|---|---|---|
mode | có | exam — tờ đề, không đáp án; answers — bảng đáp án và lời giải chi tiết |
brand | không | Id 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 |
unit | có (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_name | có | Tên kỳ thi, tối đa 150 ký tự |
duration | có | Thời gian làm bài, số nguyên phút 1–600 |
subject_name | không | Tên môn in trên đề; bỏ trống thì lấy tên môn trong danh mục |
exam_code | không | Mã đề, tối đa 10 ký tự |
header, header_right | khô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) |
footer | không | Châ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_fields | không | In dòng "Họ và tên / Số báo danh"; mặc định true |
style | không | Kiể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 |
font | không | libertinus, times, noto-serif hoặc be-vietnam-pro. Bỏ trống thì theo kiểu: classic — libertinus, modern — be-vietnam-pro, minimal — noto-serif |
color | không | Mà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.