Partner API
Integrate ExamVault's proctored exam delivery into your platform.
Overview
The ExamVault Partner API lets your platform book proctored exams for your students, receive results via webhook, and check exam status programmatically.
https://certexpert.orgAll requests and responses use JSON. Timestamps are Unix milliseconds (ms).
Authentication
Every request must include your Partner API Key in the header:
X-Partner-Key: pk_your_key_here
Error Codes
| HTTP Code | Meaning |
|---|---|
| 400 | Missing or invalid fields in request body |
| 401 | Missing or invalid X-Partner-Key |
| 403 | Exam not authorized for your org, or token belongs to another org |
| 404 | Exam or token not found |
| 409 | Conflict — duplicate active token, exam already started, or token revoked |
| 429 | Rate limit exceeded (100 bookings/hour) |
| 500 | Server error — retry after a few seconds |
Create Booking
Books an exam slot for a student. Creates the student account on ExamVault if it doesn't exist. Sends an exam link email to the student automatically.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| studentEmail | string | Required | Student's email address |
| studentName | string | Required | Student's full name |
| examId | string | Required | ExamVault exam ID (get from admin) |
| validFrom | number | Required | Exam window start — Unix ms |
| validTill | number | Required | Exam window end — Unix ms |
| gracePeriod | number | Optional | Grace minutes after window start (default: 5) |
| verificationCode | string | Optional | Up to 100 characters. If set, the candidate must enter this exact code before the exam starts within their window — an extra "we vouch for this candidate" layer on top of the link itself. Omit it and the exam starts normally, no code prompt at all. |
Example Request
POST /api/partner/create-booking
X-Partner-Key: pk_your_key_here
Content-Type: application/json
{
"studentEmail": "john@example.com",
"studentName": "John Doe",
"examId": "exam_psm1_v2",
"validFrom": 1753660200000,
"validTill": 1753663800000,
"gracePeriod": 10,
"verificationCode": "LP-7F3K9Q"
}
create-booking call, tied only to that token. It's checked at the moment the exam actually starts (including resuming after a dropped connection — the same code works, since it's the same underlying token), never while the candidate is just viewing their countdown page. A wrong code is rejected with {"error": "verification_invalid"} and can be retried; ExamVault never reveals the correct code back to you or the candidate.Success Response 200
{
"success": true,
"tokenId": "EVT-ABCD1234EFGH",
"examLink": "https://certexpert.org/exam?token=EVT-ABCD1234EFGH",
"studentUid": "firebase_uid_string"
}
Get Status
Check the current status of a booked exam token.
Status Values
| Status | Meaning |
|---|---|
| unscheduled | A voucher booked without a fixed slot (see Create Booking) — the candidate hasn't picked a date/time yet. validFrom/validTill are both null. |
| pending | Booked with a slot, exam window not yet open |
| active | Exam window is open, student can start |
| completed | Student submitted the exam |
| expired | Window closed, student did not appear |
| revoked | Token was cancelled (by you via Cancel, or by ExamVault admin) |
unscheduled, Cancel/Reschedule for pending or active, and neither once completed, expired, or revoked.Example Response
{
"tokenId": "EVT-ABCD1234EFGH",
"status": "pending",
"validFrom": 1753660200000,
"validTill": 1753663800000
}
Get Result
Fetch the exam result. Use this to poll if you missed the webhook, or to confirm result data.
Example Response
{
"status": "completed",
"tokenId": "EVT-ABCD1234EFGH",
"studentName": "John Doe",
"studentEmail": "john@example.com",
"examTitle": "PSM-1 Certification",
"score": 82,
"passed": true,
"correct": 41,
"wrong": 7,
"skipped": 2,
"timeTaken": "48m 12s",
"violations": 0,
"certId": "EV-ABC123DEF456",
"verifyUrl": "https://certexpert.org/verify?id=EV-ABC123DEF456",
"completedAt": 1753662800000
}
status will be pending or in-progress and result fields will be absent.{"status": "held", "tokenId": "...", "message": "..."} instead — no score or answer data at all, not even a partial result. Call Release Result when you're ready to reveal it. See Webhook Events for how this also delays the exam-complete webhook.Certificate
Fetch certificate details for a completed exam. Returns certificate ID, verify URL, and issued date. Use this to link back to ExamVault's verification page.
Example Response (passed)
{
"tokenId": "EVT-ABCD1234EFGH",
"certified": true,
"certId": "EV-ABC123DEF456",
"verifyUrl": "https://certexpert.org/verify?id=EV-ABC123DEF456",
"studentName": "John Doe",
"examTitle": "PSM-1 Certification",
"score": 82,
"issuedAt": 1753662800000,
"issuedDate": "27 Jul 2026"
}
Example Response (failed)
{
"tokenId": "EVT-ABCD1234EFGH",
"certified": false,
"reason": "below_pass_mark"
}
404. If the student failed, certified will be false with a reason field.{"certified": false, "reason": "held", "message": "..."} — the certificate isn't issued until the result is released, even if the student passed.Release Result
If your organization has result-holding enabled, a completed exam's score is invisible to everyone — including the candidate — until you call this. There's no automatic release and no time limit; it stays held until you decide.
Calling this reveals the score to the candidate, issues the certificate (if earned), and fires the exam-complete webhook — all three happen together, at the moment you release.
Example Response
{ "success": true, "tokenId": "EVT-ABCD1234EFGH", "certIssued": true, "webhookFired": true }
{"success": true, "alreadyReleased": true}, nothing fires twice. You can only release your own organization's results; a token belonging to another org returns 403.Reschedule
Change the exam window for a token. Only allowed if the student has not yet started the exam.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| tokenId | string | Required | Token to reschedule |
| validFrom | number | Required | New window start — Unix ms |
| validTill | number | Required | New window end — Unix ms |
Example Response
{
"success": true,
"tokenId": "EVT-ABCD1234EFGH",
"oldValidFrom": 1753700000000,
"oldValidTill": 1753703600000,
"validFrom": 1753750000000,
"validTill": 1753753600000,
"rescheduleCount": 1
}
exam-rescheduled webhook (see Webhook Payload) and, unless the booking is a sandbox token, emails the student (or your notifyEmail inbox, if configured) with the new window.Cancel
Cancel a booking. Only allowed if the student has not yet started the exam — once started or completed, the booking can no longer be cancelled.
Example Response
{ "success": true, "tokenId": "EVT-ABCD1234EFGH" }
{"success": true, "alreadyCancelled": true}. Fires the exam-cancelled webhook and, unless the booking is a sandbox token, emails the student (or your notifyEmail inbox, if configured).Webhook Events
ExamVault fires POST requests to your configured webhook URL when key events occur.
| Event | When it fires |
|---|---|
| exam-complete | Student submits exam — result available (or, if result-holding is enabled for your org, delayed until you call Release Result) |
| exam-started | Student opens the exam and begins |
| exam-rescheduled | Exam slot is moved to a new time |
| exam-scheduled | Student picks a date/time for an unscheduled voucher (see Create Booking — omitting validFrom/validTill issues an open voucher the student later schedules themselves) |
| exam-cancelled | You cancel a booking via Cancel — the booking was withdrawn before the candidate ever took it |
| exam-terminated | The candidate's attempt was disqualified mid-exam for a proctoring integrity violation — distinct from exam-cancelled, which only means the booking itself was withdrawn, not that a candidate was caught mid-attempt |
| exam-reminder-24h | Fires the same moment our own 24-hour reminder email goes out to the candidate |
| exam-reminder-1h | Fires the same moment our own 1-hour reminder email goes out to the candidate |
Webhook Payload
exam-complete payload
{
"event": "exam-complete",
"tokenId": "EVT-ABCD1234EFGH",
"orgId": "scrumintelligence",
"studentName": "John Doe",
"studentEmail": "john@example.com",
"examTitle": "PSM-1 Certification",
"examId": "exam_psm1_v2",
"score": 82,
"passed": true,
"correct": 41,
"wrong": 7,
"skipped": 2,
"timeTaken": "48m 12s",
"violations": 2,
"violationDetails": [
{ "type": "gaze", "reason": "Head/eyes looking away for an extended period — possible attempt to view unauthorised material", "timestamp": 1753662500000 },
{ "type": "audio", "reason": "Sustained loud audio detected — someone may be speaking or assisting you", "timestamp": 1753662650000 }
],
"certId": "EV-ABC123DEF456",
"verifyUrl": "https://certexpert.org/verify?id=EV-ABC123DEF456",
"completedAt": 1753662800000,
"eventId": "9f8e7d6c-5b4a-3210-9876-fedcba098765",
"sentAt": 1753662801234
}
violations is the total count; violationDetails is the same events broken out with type (gaze, audio, multiple-faces, no-face, or keyboard), a human-readable reason, and the exact timestamp each was recorded — in chronological order. Empty array if there were none.exam-started payload
{
"event": "exam-started",
"tokenId": "EVT-ABCD1234EFGH",
"orgId": "scrumintelligence",
"studentEmail": "john@example.com",
"studentName": "John Doe",
"examId": "exam_psm1_v2",
"examTitle": "PSM-1 Certification",
"startedAt": 1753662700000,
"eventId": "3c2b1a09-8f7e-6d5c-4b3a-2918f7e6d5c4",
"sentAt": 1753662701234
}
exam-rescheduled payload
{
"event": "exam-rescheduled",
"tokenId": "EVT-ABCD1234EFGH",
"orgId": "scrumintelligence",
"studentEmail": "john@example.com",
"studentName": "John Doe",
"examId": "exam_psm1_v2",
"examTitle": "PSM-1 Certification",
"oldValidFrom": 1753600000000,
"oldValidTill": 1753610000000,
"newValidFrom": 1753700000000,
"newValidTill": 1753710000000,
"rescheduleCount": 1,
"rescheduledBy": "student",
"eventId": "7a6b5c4d-3e2f-1a0b-9c8d-7e6f5a4b3c2d",
"sentAt": 1753662801234
}
rescheduledBy is "student" (self-service reschedule), "admin" (rescheduled on your behalf by ExamVault staff), or "partner" (you called Reschedule yourself).exam-scheduled payload
{
"event": "exam-scheduled",
"tokenId": "EVT-ABCD1234EFGH",
"orgId": "scrumintelligence",
"studentEmail": "john@example.com",
"studentName": "John Doe",
"examId": "exam_psm1_v2",
"examTitle": "PSM-1 Certification",
"validFrom": 1753700000000,
"validTill": 1753710000000,
"eventId": "1b2c3d4e-5f6a-7b8c-9d0e-1f2a3b4c5d6e",
"sentAt": 1753662801234
}
validFrom/validTill (see Create Booking). If you always supply a fixed slot, you'll never receive this event — you already know the window at booking time.exam-cancelled payload
{
"event": "exam-cancelled",
"tokenId": "EVT-ABCD1234EFGH",
"orgId": "scrumintelligence",
"studentEmail": "john@example.com",
"studentName": "John Doe",
"examId": "exam_psm1_v2",
"examTitle": "PSM-1 Certification",
"cancelledAt": 1753662801234,
"eventId": "9d8c7b6a-5e4f-3210-9876-543210fedcba",
"sentAt": 1753662801234
}
exam-terminated payload
{
"event": "exam-terminated",
"tokenId": "EVT-ABCD1234EFGH",
"orgId": "scrumintelligence",
"studentEmail": "john@example.com",
"studentName": "John Doe",
"examId": "exam_psm1_v2",
"examTitle": "PSM-1 Certification",
"reason": "Integrity violation — second offense",
"terminatedAt": 1753662801234,
"eventId": "9d8c7b6a-5e4f-3210-9876-543210fedcba",
"sentAt": 1753662801234
}
event values rather than erroring, the same way you should for any future event we add.exam-reminder-24h / exam-reminder-1h payload
{
"event": "exam-reminder-24h",
"tokenId": "EVT-ABCD1234EFGH",
"orgId": "scrumintelligence",
"studentEmail": "john@example.com",
"studentName": "John Doe",
"examId": "exam_psm1_v2",
"examTitle": "PSM-1 Certification",
"validFrom": 1753662801234,
"validTill": 1753666401234,
"eventId": "9d8c7b6a-5e4f-3210-9876-543210fedcba",
"sentAt": 1753662801234
}
notifyEmail inbox instead, if that's configured) — exam-reminder-24h when their exam is within 24 hours, exam-reminder-1h when it's within 1 hour. Only fires for a booking that's actually scheduled with a fixed window; an unscheduled voucher never reaches either milestone until the candidate picks a time.Headers
| Header | Value |
|---|---|
| X-CertExpert-Event | Same as the event field — lets you route without parsing the body first |
| X-CertExpert-Event-Id | Same as the eventId field |
| X-CertExpert-Signature | sha256=<hmac> — HMAC-SHA256 of the raw body using your webhook secret, present only if a secret is configured for your key |
Deduplication
eventId uniquely identifies one occurrence of an event and stays identical across every retry attempt of that same occurrence. If your endpoint receives an eventId it has already processed, treat it as a duplicate delivery and skip reprocessing — this is expected behavior for a failed attempt that later succeeds, not a new event.Retry Logic
If delivery fails (network error or non-2xx response), ExamVault retries automatically:
| Attempt | Delay after failure |
|---|---|
| 1st (initial) | Immediate |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours |
GET /api/partner/result/:tokenId to poll manually if needed.