Mock Sign in

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

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.

  1. Make sure the exam has a class (in Mock) and is active.
  2. 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.
  3. Redirect the student's browser to the link. It works once and expires after 5 minutes. A student your teacher blocked is refused.
  4. The exam then runs as usual: the timer, strikes and marking are the same as for a student who typed a code.
  5. 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
}
statusMeaning
in_progressStarted, not submitted yet.
gradingSubmitted. Marking is running.
pending_reviewSubmitted. A teacher still has to mark some answers. The score is not final.
gradedMarks are final.

Errors and limits

Errors have the same shape and an HTTP status:

{ "error": { "code": "not_found", "message": "No exam with that id." } }
StatuscodeWhen
400invalid_requestMissing or malformed input.
401invalid_api_keyNo key, a wrong key, or a revoked key.
403plan_inactiveThe school plan behind the key is not active.
404not_foundUnknown path, or a class or exam that is not yours.
405method_not_allowedWrong method. The Allow header lists the right one.
422unprocessableThe request is valid but cannot be done (exam not active, no class, staff email).
429rate_limitedMore 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:

typeWhen
exam.submittedThe student's submission was received. The score may still be waiting for marking.
result.gradedThe 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
  }
}

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.