Mock API
For schools with their own systems. Add students, open an exam for a student with a one-time link, and read the results, without anyone typing an exam code.
Overview
- Base address:
https://mockassessment.com/api/v1. Everything is JSON over HTTPS. - For servers only. There is no browser access (no CORS), so a key must never be put in a web page or an app.
- A key belongs to a School Head and acts as that School Head. It reaches your own classes and exams, not those of teachers linked to your school.
- Create keys on the Integrations page (School Head, in the sidebar). The school plan must be active.
- This API does not create or edit exams or questions. Build the exam in Mock, then drive it from your system.
Authentication
Send your key in the Authorization header on every request. Only a hash of the key is stored, so a lost key cannot be recovered: create a new one and revoke the old one.
curl https://mockassessment.com/api/v1/ping \
-H "Authorization: Bearer mk_live_..."
Opening an exam for a student
Your system already knows who the student is, so Mock does not ask for an email and exam code.
- Make sure the exam has a class (in Mock) and is active.
- When the student clicks "Take exam" in your system, your server calls Launch an exam. Mock adds the student to the class if needed and returns a link.
- Redirect the student's browser to the link. It works once and expires after 5 minutes. A student your teacher blocked is refused.
- The exam then runs as usual: the timer, strikes and marking are the same as for a student who typed a code.
- When marks are final Mock calls your webhook, or you read Results.
GET/ping
Checks that your key works.
{ "ok": true, "integration": { "id": "k3...", "name": "Moodle server" } }
GET/classes
Your classes, with the ids you need to add students.
{ "data": [ { "id": "c1...", "name": "Form 4", "units": ["Physics"], "studentCount": 31 } ] }
GET/exams
Your exams, 50 at a time. Pass limit (1 to 100) and after (the next value of the previous page). No questions, answers or exam codes are returned.
{
"data": [ {
"id": "e1...", "title": "Physics mock", "classId": "c1...", "unit": "Physics",
"active": true, "durationMinutes": 30, "questionCount": 40,
"scheduledStart": null, "scheduledEnd": null
} ],
"next": null
}
POST/classes/{classId}/students
Adds students to one of your classes, up to 200 per call. A student already on your roster is updated, never duplicated or renamed. A staff email cannot be added as a student.
curl -X POST https://mockassessment.com/api/v1/classes/c1.../students \
-H "Authorization: Bearer mk_live_..." -H "Content-Type: application/json" \
-d '{ "students": [ { "email": "ann@school.edu", "name": "Ann Achieng" } ] }'
{ "created": 1, "updated": 0, "failed": 0, "failures": [] }
Each student may also carry "unit". If it is left out, the class's first unit is used.
POST/exams/{examId}/launch
Returns a one-time link that opens this exam for one student. The student is added to the exam's class if they are not in it yet.
curl -X POST https://mockassessment.com/api/v1/exams/e1.../launch \
-H "Authorization: Bearer mk_live_..." -H "Content-Type: application/json" \
-d '{ "student": { "email": "ann@school.edu", "name": "Ann Achieng" } }'
201
{
"launchUrl": "https://mockassessment.com/join?launch=...",
"expiresAt": "2026-10-27T10:05:00.000Z",
"student": { "email": "ann@school.edu" },
"exam": { "id": "e1...", "title": "Physics mock" }
}
The call is refused with 422 when the exam is not active, has no class, or the email belongs to a staff account.
GET/exams/{examId}/results
Marks and status for the exam, 50 at a time (limit, after as for exams). Add ?email=ann@school.edu to read one student's result. Answers and the marking itself are never returned.
{
"data": [ {
"resultId": "e1..._ann@school.edu", "examId": "e1...",
"student": { "email": "ann@school.edu", "name": "Ann Achieng" },
"score": 31, "totalMarks": 40, "percentage": 77.5,
"status": "graded",
"submittedAt": "2026-10-27T09:41:10.000Z", "timeTakenSeconds": 1520
} ],
"next": null
}
| status | Meaning |
|---|---|
in_progress | Started, not submitted yet. |
grading | Submitted. Marking is running. |
pending_review | Submitted. A teacher still has to mark some answers. The score is not final. |
graded | Marks are final. |
Errors and limits
Errors have the same shape and an HTTP status:
{ "error": { "code": "not_found", "message": "No exam with that id." } }
| Status | code | When |
|---|---|---|
| 400 | invalid_request | Missing or malformed input. |
| 401 | invalid_api_key | No key, a wrong key, or a revoked key. |
| 403 | plan_inactive | The school plan behind the key is not active. |
| 404 | not_found | Unknown path, or a class or exam that is not yours. |
| 405 | method_not_allowed | Wrong method. The Allow header lists the right one. |
| 422 | unprocessable | The request is valid but cannot be done (exam not active, no class, staff email). |
| 429 | rate_limited | More than 120 requests a minute for a key. Wait for the Retry-After seconds. |
A revoked key can keep working for up to 30 seconds while servers refresh their view of it.
Webhooks
Set a webhook address on the Integrations page. Mock sends a POST with a JSON body when something happens to a result of one of your exams:
| type | When |
|---|---|
exam.submitted | The student's submission was received. The score may still be waiting for marking. |
result.graded | The marks are final, and again whenever a teacher's change alters the score. |
{
"id": "evt_ce598fcb12df6fa8c7b1af43",
"type": "result.graded",
"createdAt": "2026-10-27T09:41:12.000Z",
"data": {
"resultId": "e1..._ann@school.edu", "examId": "e1...",
"student": { "email": "ann@school.edu", "name": "Ann Achieng" },
"score": 31, "totalMarks": 40, "percentage": 77.5, "status": "graded",
"submittedAt": "2026-10-27T09:41:10.000Z", "timeTakenSeconds": 1520
}
}
- Answer with any
2xxstatus. Mock tries up to 3 times (now, after 2 seconds, after 5 more) and does not retry a4xxother than 408 and 429. - The same event keeps the same
idwhen it is retried, so store it and ignore repeats. - Delivery is best effort. If your server was down, read Results to catch up.
- The address must be a public
https://address. Redirects are not followed.
Verify a webhook
Each request carries Mock-Signature: t=<unix seconds>,v1=<hex>. The value is an HMAC-SHA256, keyed with your signing secret, of the text t + "." + raw body. Compute it over the raw body exactly as received, compare in constant time, and reject a t more than 5 minutes old.
// Node.js
const crypto = require('crypto');
function verify(secret, rawBody, header) {
const m = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header || '');
if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
const want = crypto.createHmac('sha256', secret).update(m[1] + '.' + rawBody).digest();
return crypto.timingSafeEqual(want, Buffer.from(m[2], 'hex'));
}
// PHP
function verify($secret, $rawBody, $header) {
if (!preg_match('/^t=(\d+),v1=([a-f0-9]{64})$/', $header, $m)) return false;
if (abs(time() - (int)$m[1]) > 300) return false;
return hash_equals(hash_hmac('sha256', $m[1] . '.' . $rawBody, $secret), $m[2]);
}
Use Send a test event on the Integrations page to check your receiver. It sends a signed webhook.test event.