You add proctoring to a web app you own in three steps: your backend mints a session with one API call, a small browser loader runs the exam and captures the candidate, and your backend reads a server-side integrity report when the exam ends. Your API key stays on your server. The browser only ever holds a short-lived session token, and candidates install nothing. The rest of this page is the working integration, using only calls from the ProctorLink API reference.
Schedule a DemoProctoring a custom application is a split between two places. A backend call cannot see the candidate, and a browser tab cannot hold a secret safely, so the work divides cleanly along that line. Your server does the two things a server can do, mint the session before the exam and read the report after it, and the browser does the one thing only a browser can do, capture the candidate.
| Part | Where it runs | Call | Credentials it holds |
|---|---|---|---|
| 1. Mint a session | Your backend | POST /v1/sessions | access-token and secret-token (server-side only) |
| 2. Run the exam | Candidate browser | ProctorLink.createSession(...).start() | Only the short-lived session_jwt |
| 3. Read the report | Your backend | GET /v1/sessions/:id | access-token and secret-token (server-side only) |
If you are still deciding whether the SDK route is right for you at all, rather than the Moodle plugin or an LTI 1.3 launch, start with Proctoring SDK vs API vs LMS plugin. If you are new to the field, read What is online proctoring? and What is AI proctoring? first, then come back here to build.
One session covers one quiz attempt. Your backend calls POST /v1/sessions with your server credentials and an identifier for the candidate. The attempt_id is optional in the schema but strongly recommended, because it is what lets a session resume after a refresh or a network drop.
// Your backend. Your API key never enters the browser.
POST /v1/sessions
access-token: <YOUR_ACCESS_TOKEN>
secret-token: <YOUR_SECRET_TOKEN>
content-type: application/json
{
"external_user_id": "candidate-123",
"exam_id": "math-101-final",
"attempt_id": "attempt-789",
"allowed_origins": ["https://exams.yourcompany.com"]
}
// Response
{
"session_id": "6a7b18df70e4f8ecf2597b6f",
"session_jwt": "eyJhbGciOi...",
"expires_at": 1786443201
}The session_jwt is a short-lived token scoped to this one attempt, and it is the only value you pass to the browser. Your access-token and secret-token stay on your server. The token lasts two hours by default. Set ttl_seconds to your exam length plus a buffer, and read the returned expires_at, because values outside the allowed range are clamped rather than rejected.
Install the SDK and hand it the token your backend just minted. In the default flow, recording begins as soon as the camera is granted and the first captured frame becomes the identity reference. That is the entire browser integration.
npm install @proctorlink/sdk@^0.1.3import { ProctorLink } from '@proctorlink/sdk';
// jwt and sessionId come from the POST /v1/sessions response above,
// fetched from your own backend endpoint.
const session = ProctorLink.createSession({
jwt: sessionJwt, // session_jwt
sessionId, // session_id
});
session.on('permission', ({ camera }) => {
if (camera === 'denied') { /* block or warn, your policy */ }
});
session.onEvent((event) => console.log(event.type, event));
await session.start();
// when the attempt finishes:
session.stop();
session.destroy();Candidates install nothing. This is Tier 1, the browser-only tier: it observes camera, microphone, focus and blur, fullscreen state, clipboard, and device changes. If you want matching against a deliberate, well-framed photo rather than whatever the first frame caught, create the session with autoStartCapture: false, call captureIdentity() after start() resolves, then call beginCapture() to start the exam.
If your page sets a Content-Security-Policy, allow the enclave origin so the iframe can mount:
frame-src https://enclave.proctorlink.com;Two events are worth wiring up deliberately. The permission event tells you whether the candidate granted the camera, so you can enforce your own policy. The token-expired event fires if a token dies mid-attempt, so capture never fails silently. Re-mint for the same attempt_id, which resumes the session, and call updateToken() with the fresh token.
// The token is short-lived. If it expires mid-exam, re-mint for the
// SAME attempt_id, which resumes the session, then hand the SDK the new token.
session.on('token-expired', async () => {
const res = await mintSession({ attempt_id: attemptId });
if (res.resumed) {
session.updateToken(res.session_jwt); // same session, carry on
} else {
// The old session ended. This is a fresh attempt with its own report.
session.destroy();
session = ProctorLink.createSession({
jwt: res.session_jwt,
sessionId: res.session_id,
});
await session.start();
}
});Events recorded while the token was dead are queued and flushed as soon as the new token lands, so nothing from the gap is lost. Check resumed before calling updateToken(): a re-mint only resumes while the session is still active, and if the abandonment sweep has already closed it, the mint creates a new session whose token belongs to a different attempt. Do not call stop() or destroy() just to refresh a token, because that ends the attempt server-side and the next mint bills a second session.
After the exam, your backend calls GET /v1/sessions/:id. Face analysis is deferred, not real-time, so fetch the report once status is validated, or equivalently when integrity.analysis_complete is true. Before that, face counts are zero and identity_match is pending, which is an unfinished result rather than a clean one.
// Your backend. Fetch once status is "validated"
// (or integrity.analysis_complete is true).
GET /v1/sessions/6a7b18df70e4f8ecf2597b6f
access-token: <YOUR_ACCESS_TOKEN>
secret-token: <YOUR_SECRET_TOKEN>
// Response (trimmed)
{
"status": "validated",
"integrity": {
"score": 100,
"level": "low",
"flagged": false,
"identity_match": "pass",
"reasons": []
}
}The integrity.score starts at 100 and drops as penalties apply. The level is low at 80 or above, medium from 50 to 79, and high below 50, and flagged turns true below 80. The verdict is computed server-side only, so the candidate cannot influence it. A score alone should not fail anyone: use reasons and evidence to drive human review of anything flagged.
The SDK route runs entirely in the tab with no install, which covers the signals a browser can legitimately observe. Deeper inspection needs more privileged software, which is why the deployment tiers exist. Pick the tier that matches your risk, not the deepest one available.
| Tier | Candidate install | What it can observe |
|---|---|---|
| Tier 1: Browser SDK | None | Camera, microphone, focus and blur, fullscreen state, clipboard, device changes |
| Tier 2: Browser extension | Extension | Adds tab enumeration, per-application focus, and download blocking |
| Tier 3: Desktop agent | Desktop app | Process inspection, display enumeration, screen-recording detection, and full lockdown |
A browser tab cannot see other applications, a second device, a virtual machine, or a remote-desktop session. That is a property of the web platform, not a gap in any one vendor, and it is exactly why the extension and desktop tiers exist. Face analysis is probabilistic too: the report counts frames where no face was detected, which is a signal for a reviewer, not proof that a candidate left the room.
access-token and secret-token are server-side credentials. Anyone holding them can mint sessions billed to your account and read your reports. Fix: mint on your backend and pass only the session_jwt to the SDK.attempt_id on page load. Resume keys off attempt_id, so a new value on every refresh splits one exam into several sessions and bills the attempt more than once. Fix: derive attempt_id from your own attempt record so it is stable for the attempt.identity_match of pending. Fix: fetch when status is validated, polling on a sensible interval rather than a tight loop.frame-src rule. If your page sets a Content-Security-Policy and does not allow the enclave origin, the iframe cannot mount and capture never starts. Fix: add the enclave origin to frame-src.stop() or destroy() to get a new token ends the attempt server-side, so the next mint creates a separate session: two reports for one attempt, billed twice. Fix: handle token-expired and call updateToken() with a freshly minted token for the same attempt_id.The security model is worth understanding before you build. Your API key is server-side only. The browser receives a short-lived, origin-bound session token, and that token lives inside a cross-origin iframe enclave served from ProctorLink’s own domain rather than in host-page JavaScript. The camera permission binds to the enclave origin, so a candidate who has already granted it on one exam is not prompted again on another site that embeds ProctorLink. The integrity score is computed server-side only, which is the core anti-tamper property: a candidate cannot edit their own score no matter what they do in the browser.
Keyframes upload from the browser straight to object storage through presigned URLs, so image bytes never transit the API, and only periodic stills are captured rather than continuous video. If your exams instead run inside Moodle, you can skip this integration entirely and use the drop-in plugin covered in Best Moodle proctoring plugin. Across published deployments, ProctorLink has supported more than one million proctored exam sessions (methodology note below). Full cohort sizes and outcomes are on the case studies page.
Institutions evaluating proctoring tools often look for independent feedback outside vendor case studies. ProctorLink is listed on G2, where Moodle administrators and training teams share verified product reviews.
Read ProctorLink reviews on G2 →Deployment statistics and product behaviour described in this guide link to the sources below.
Ready to add proctoring to software you already own? Get credentials, then wire the three calls above into a real exam and check the reviewer workload before you commit.
See the browser SDK and integrity report in a 30-minute developer walkthrough.
Get SDK credentials and the full API reference for the mint and report calls.
Per-session and subscription models for custom applications and LMS exams.
A ready-made assessment platform if you would rather not build your own.
The decision guide for picking the SDK route over the plugin or an LTI launch.
Back to the knowledge hub for the full SDK and proctoring series.
Online proctoring supervises remote exams via webcam and microphone inside your LMS, logging rule violations with timestamps so reviewers judge flagged cases, not every session.
AI proctoring uses machine learning to automatically detect suspicious exam behaviour such as multiple faces, tab switching, and absence from the camera.
A comparison of leading Moodle proctoring plugins for universities that need native LMS integration, AI monitoring, and institution-owned data.
How university exam offices choose online proctoring software for entrance grids, finals, and multi-faculty calendars, including stakeholder RFPs and centre-vs-online cost tradeoffs.
How certification bodies and training providers use online proctoring for identity proofing, item-bank protection, and defensible, audit-ready credentialing exams.
A side-by-side comparison of AI proctoring and live human proctoring for cost, scale, accuracy, and exam security.
Practical steps to reduce cheating in Moodle quizzes using proctoring settings, question banks, timing controls, and AI monitoring.
Feature-by-feature comparison of Moodle proctoring plugins covering integration, AI detection, pricing, and data storage.
A guide to online exam proctoring for Indian universities and certification providers, including compliance, pricing in INR, and local case studies.
How to choose between a proctoring LMS plugin, a browser SDK, and a REST API when adding proctoring to software you already own, with a decision guide and code examples.
Your backend mints a session, a small browser loader runs it, and you read a server-side integrity score. Pilot the SDK on a real exam and check the reviewer workload before you commit.