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.

Base URL: https://certexpert.org
All 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
⚠ Keep your API key secret. Do not expose it in frontend code or public repositories. Contact ExamVault admin to rotate a compromised key.

Error Codes

HTTP CodeMeaning
400Missing or invalid fields in request body
401Missing or invalid X-Partner-Key
403Exam not authorized for your org, or token belongs to another org
404Exam or token not found
409Conflict — duplicate active token, exam already started, or token revoked
429Rate limit exceeded (100 bookings/hour)
500Server error — retry after a few seconds

Create Booking

POST /api/partner/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

FieldTypeRequiredDescription
studentEmailstringRequiredStudent's email address
studentNamestringRequiredStudent's full name
examIdstringRequiredExamVault exam ID (get from admin)
validFromnumberRequiredExam window start — Unix ms
validTillnumberRequiredExam window end — Unix ms
gracePeriodnumberOptionalGrace minutes after window start (default: 5)
verificationCodestringOptionalUp 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"
}
The code is single-use per booking — generate a fresh one for each 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"
}
Store the tokenId — it's your reference for polling status, fetching results, and rescheduling.

Get Status

GET /api/partner/status/:tokenId

Check the current status of a booked exam token.

Status Values

StatusMeaning
unscheduledA voucher booked without a fixed slot (see Create Booking) — the candidate hasn't picked a date/time yet. validFrom/validTill are both null.
pendingBooked with a slot, exam window not yet open
activeExam window is open, student can start
completedStudent submitted the exam
expiredWindow closed, student did not appear
revokedToken was cancelled (by you via Cancel, or by ExamVault admin)
This is what drives Schedule vs. Cancel/Reschedule button logic on your side: show Schedule for 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

GET /api/partner/result/:tokenId

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
}
If exam is not yet completed, status will be pending or in-progress and result fields will be absent.
If your organization has result-holding enabled (ask ExamVault admin to turn this on for your key), a completed exam returns {"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

GET /api/partner/certificate/:tokenId

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"
}
If no result exists yet, returns 404. If the student failed, certified will be false with a reason field.
If the result is held (see Get Result), this returns {"certified": false, "reason": "held", "message": "..."} — the certificate isn't issued until the result is released, even if the student passed.

Release Result

POST /api/partner/release-result/:tokenId

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 }
Safe to call more than once — releasing an already-released result just returns {"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

POST /api/partner/reschedule

Change the exam window for a token. Only allowed if the student has not yet started the exam.

Request Body

FieldTypeRequiredDescription
tokenIdstringRequiredToken to reschedule
validFromnumberRequiredNew window start — Unix ms
validTillnumberRequiredNew window end — Unix ms

Example Response

{
  "success": true,
  "tokenId": "EVT-ABCD1234EFGH",
  "oldValidFrom": 1753700000000,
  "oldValidTill": 1753703600000,
  "validFrom": 1753750000000,
  "validTill": 1753753600000,
  "rescheduleCount": 1
}
Also fires the 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

POST /api/partner/cancel/:tokenId

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" }
Safe to call more than once — cancelling an already-cancelled token just returns {"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.

EventWhen it fires
exam-completeStudent submits exam — result available (or, if result-holding is enabled for your org, delayed until you call Release Result)
exam-startedStudent opens the exam and begins
exam-rescheduledExam slot is moved to a new time
exam-scheduledStudent picks a date/time for an unscheduled voucher (see Create Booking — omitting validFrom/validTill issues an open voucher the student later schedules themselves)
exam-cancelledYou cancel a booking via Cancel — the booking was withdrawn before the candidate ever took it
exam-terminatedThe 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-24hFires the same moment our own 24-hour reminder email goes out to the candidate
exam-reminder-1hFires the same moment our own 1-hour reminder email goes out to the candidate

Webhook Payload

POST your-webhook-url

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.
Your endpoint must return HTTP 2xx within 10 seconds. Any other response triggers a retry.

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
}
Fires once, the moment a student picks a date/time for a voucher you issued without 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
}
Fires when you call Cancel. Never fires on its own — cancellation only ever happens through that endpoint.

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
}
Fires the moment a candidate's proctoring session hits its second integrity violation (tab switch, fullscreen exit, multiple faces, etc.) — the attempt is disqualified server-side immediately and the token can never be resumed or restarted. No score is included: there is none, the exam was cut short. This event is new — if your webhook handler doesn't recognize it, ignore unknown 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
}
Fires at the exact same moment our own reminder email goes out to the candidate (or to your 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

HeaderValue
X-CertExpert-EventSame as the event field — lets you route without parsing the body first
X-CertExpert-Event-IdSame as the eventId field
X-CertExpert-Signaturesha256=<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:

AttemptDelay after failure
1st (initial)Immediate
2nd retry5 minutes
3rd retry30 minutes
4th retry2 hours
After 4 attempts with no delivery, no further retries occur. Use GET /api/partner/result/:tokenId to poll manually if needed.