Endpoint details
Every endpoint, the scope it needs, and what to know before calling it. The API Reference section has the full field-by-field description of each one, generated from the OpenAPI document.
All paths below follow https://your-site.example/api/index.php/v1/nextexams.
Endpoints
| Method | Path | Scope | Does |
|---|---|---|---|
GET |
/subjects |
subjects:read |
List subjects |
POST |
/subjects |
subjects:write |
Create a subject |
GET |
/subjects/{id} |
subjects:read |
Get a subject |
PATCH |
/subjects/{id} |
subjects:write |
Update a subject |
DELETE |
/subjects/{id} |
subjects:delete |
Delete a subject |
POST |
/subjects/{id}/status |
subjects:write |
Publish or unpublish |
GET |
/tags |
questions:read |
List tags |
POST |
/tags |
questions:write |
Create a tag |
GET |
/tags/{id} |
questions:read |
Get a tag |
PATCH |
/tags/{id} |
questions:write |
Update a tag |
DELETE |
/tags/{id} |
questions:delete |
Delete a tag |
POST |
/tags/{id}/status |
questions:write |
Publish or unpublish |
GET |
/questions |
questions:read |
List bank questions |
POST |
/questions |
questions:write |
Create a question |
GET |
/questions/{id} |
questions:read |
Get a question with its options |
PUT |
/questions/{id} |
questions:write |
Replace a question |
DELETE |
/questions/{id} |
questions:delete |
Delete (or retire) a question |
POST |
/questions/{id}/status |
questions:write |
Publish or unpublish |
GET |
/questions/{id}/versions |
questions:read |
A question's saved versions |
GET |
/question-types |
lookups:read |
The question types this site offers |
GET |
/exams |
exams:read |
List exams |
GET |
/exams/{id} |
exams:read |
Get an exam with its structure |
DELETE |
/exams/{id} |
exams:delete |
Delete an exam |
POST |
/exams/{id}/status |
exams:publish |
Publish or unpublish |
GET |
/attempts |
attempts:read |
List attempts |
GET |
/attempts/{id} |
attempts:read |
Get an attempt with every response |
GET |
/reports/{exam_id}/overview |
reports:read |
Pass rate and score distribution |
GET |
/reports/{exam_id}/items |
reports:read |
Item analysis |
GET |
/reports/{exam_id}/students |
reports:read |
One row per examinee |
The scope is the first check. The token owner's role is checked as well: listing attempts also needs the role's attempts.view, deleting an exam its exams.delete, and so on.
Subjects
Subjects form a tree. parent_id is 0 at the top level. path is the ids from the root down, joined by /, and level counts from 1.
Moving a subject (a PATCH with a new parent_id) rebuilds the path of its whole subtree. A move under one of its own descendants is refused.
curl -X POST https://your-site.example/api/index.php/v1/nextexams/subjects \
-H "Authorization: Bearer nxk_your_token" \
-H "Content-Type: application/json" \
-d '{"title": "Algebra", "parent_id": 0, "code": "MATH-ALG"}'
{ "data": { "id": 42 } }
Questions
Lists show the bank
GET /questions lists bank questions. Two kinds are left out:
- Questions private to one exam, made in the Exam Studio with Keep in bank off. They are part of that exam and appear in its structure.
- Retired versions, kept only so old attempts can still be read.
owner_exam_idis-1on these.
GET /questions/{id} reads any question by id, whichever kind it is.
Where the answer lives
| Question types | The answer is in |
|---|---|
| Multiple choice, true/false, image choice, ordering | options: each option's is_correct (and its order, for ordering) |
| Matching, EMQ | options, with each item's partner in match_key |
| Short answer | answer_key: accepted (a list), plus case_insensitive, trim, numeric / tolerance, use_regex |
| Essay | Graded by a person. answer_key holds word limits and grading notes |
| Cloze, range, hotspot, image marking, graph, puzzle | answer_key, in the type's own shape |
The surest way to learn a type's shape is to build one such question in the editor and read it back with GET /questions/{id}. That body is exactly what POST and PUT accept.
GET /question-types lists every question_type value, with its label in the site language. The advanced and visual types can be switched off in the App Manager; creating a new question of a switched-off type is then refused, while existing ones keep working.
Creating one
curl -X POST https://your-site.example/api/index.php/v1/nextexams/questions \
-H "Authorization: Bearer nxk_your_token" \
-H "Content-Type: application/json" \
-d '{
"title": "Two plus two",
"question_type": "mcq_single",
"stem": "<p>What is 2 + 2?</p>",
"default_points": 1,
"difficulty": 1,
"tags": ["Arithmetic"],
"options": [
{ "option_text": "3", "is_correct": false },
{ "option_text": "4", "is_correct": true }
]
}'
tags takes tag ids, tag titles, or {id, title} objects. A title no tag has yet creates the tag.
Replacing one
Read it, change it, send the whole thing back. See Create, update and replace for why questions are replaced rather than patched, and Questions that have been answered for what Next Exams protects once attempts exist.
curl https://your-site.example/api/index.php/v1/nextexams/questions/42 \
-H "Authorization: Bearer nxk_your_token" > q.json
# edit q.json: data.title, data.stem, … then send data back
jq '.data' q.json | curl -X PUT https://your-site.example/api/index.php/v1/nextexams/questions/42 \
-H "Authorization: Bearer nxk_your_token" \
-H "Content-Type: application/json" \
--data-binary @-
Deleting one
| The question is | Result |
|---|---|
| Used by an exam | Refused, 409 QUESTION_IN_USE, with the exams in meta |
| Only referenced by past attempts | Retired, not deleted, so those attempts keep their record. Answers 204 |
| Unused | Deleted. Answers 204 |
Exams
GET /exams/{id} returns the exam's settings and its structure:
sections, each one eitherfixed(the questions placed in it, inquestions) orpool(drawn at random by itspool_rules).in_progress_attempts: how many people are sitting it right now.
Exams are built in the Exam Studio. Through the API you can read them, publish and unpublish them, and delete them.
Attempts
GET /attempts lists attempts newest first, and filters by exam_id and status:
| Status | Meaning |
|---|---|
in_progress |
Being sat now (a paused attempt is still in_progress) |
submitted |
Handed in; a question still waits for a person to grade it |
graded |
Fully graded. score, percent and passed are final |
locked_review |
Handed in and locked by the exam's lock policy: the examinee cannot start another attempt until it is released |
abandoned |
Closed without being handed in |
GET /attempts/{id} adds every response — what was answered, whether it was right, the points, and whether a person or Next Exams graded it — and the attempt's integrity signals.
Integrity signals are detection only. Focus losses, pastes, tab switches and fullscreen exits are counted, never judged. Treat them as a reason to look, not as a verdict.
A guest's attempt has user_id: null and carries the name and email the guest gave, if the exam asked for them.
Reports
All three take the exam's id in the path, and count graded attempts.
| Endpoint | Returns |
|---|---|
/reports/{exam_id}/overview |
Attempts, pass count and rate, mean, median, minimum, maximum and standard deviation of the percent, average time, and a histogram in 10-percent bands |
/reports/{exam_id}/items |
Per question: responses, difficulty (share answered correctly, 0 to 1), the same as a percent, discrimination (item-total Pearson correlation, −1 to 1), average time |
/reports/{exam_id}/students |
Per examinee: attempts, best and average percent, passing attempts. Guests are grouped into one row with no user id |
A question with low or negative discrimination is one that strong candidates get wrong as often as weak ones, which usually means it is ambiguous or mis-keyed.
Reports need the Analytics app. With it off they answer 403 APP_DISABLED.