You add proctoring to a React app with the same framework-agnostic SDK everyone else uses: mint a session on your backend, then call ProctorLink.createSession({ jwt, sessionId }).start() inside a useEffect on mount and call stop() then destroy() in the cleanup on unmount. There is no React-specific package. The work is holding the session in a ref, wiring events once, and letting effect cleanup end the attempt. Every call below comes from the ProctorLink API reference.
The SDK is imperative and the camera lives in a cross-origin iframe enclave that mounts itself, so it does not belong in your render tree. It belongs in an effect. React’s job is to tell you when the exam component appears and disappears; the SDK’s job is to start when it appears and tear down when it goes. Map each SDK call to the React moment that should trigger it and the integration writes itself.
| React moment | What you do | Call |
|---|---|---|
| Before the exam renders | Mint a session on your backend | POST /v1/sessions |
| useEffect on mount | Create the session and begin capture | ProctorLink.createSession({ jwt, sessionId }).start() |
| useEffect cleanup on unmount | Flush and tear down | stop() then destroy() |
| On the token-expired event | Re-mint for the same attempt_id, swap the token | updateToken(jwt) |
| After the candidate submits | Read the integrity report on your backend | GET /v1/sessions/:id |
This is the same three-part split covered in How to add proctoring to a web application, applied to React’s lifecycle. If you are still choosing between the SDK, the REST API, and the Moodle plugin, start with Proctoring SDK vs API vs LMS plugin. New to the field? Read What is online proctoring? and What is AI proctoring? first.
A React component cannot hold your API key safely, so minting stays on the server. In a Next.js App Router project this is a route handler or server action; in a separate backend it is an endpoint your React app calls. Send a stable attempt_id derived from your own attempt record, because that is what lets a refresh resume the same session instead of starting a new one.
// Your backend, or a Next.js server route / server action.
// The access-token and secret-token never reach 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 — pass only these to the React client
{
"session_id": "6a7b18df70e4f8ecf2597b6f",
"session_jwt": "eyJhbGciOi...",
"expires_at": 1786443201
}The browser only ever receives the session_jwt and session_id. Your access-token and secret-token stay on the 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.
Put the whole lifecycle in one custom hook so your exam component stays about questions, not cameras. Create the session inside a useEffect, hold it in a useRef rather than state, wire the events once, and return a cleanup that ends the attempt. The ref keeps the SDK instance stable while the rest of your UI re-renders.
npm install @proctorlink/sdk@^0.1.3// useProctoring.ts
import { useEffect, useRef } from 'react';
import { ProctorLink } from '@proctorlink/sdk';
type RemintResult = { session_jwt: string; session_id: string; resumed?: boolean };
type UseProctoringArgs = {
jwt: string; // session_jwt from your backend
sessionId: string; // session_id from your backend
attemptId: string; // stable per attempt, from your own record
remint: (attemptId: string) => Promise<RemintResult>;
};
export function useProctoring({ jwt, sessionId, attemptId, remint }: UseProctoringArgs) {
const sessionRef = useRef<ReturnType<typeof ProctorLink.createSession> | null>(null);
useEffect(() => {
const session = ProctorLink.createSession({ jwt, sessionId });
sessionRef.current = session;
session.on('permission', ({ camera }) => {
if (camera === 'denied') { /* block or warn, your policy */ }
});
session.onEvent((event) => console.log(event.type, event));
// Safety net for exams that outlast the token.
session.on('token-expired', async () => {
const res = await remint(attemptId);
if (res.resumed) {
session.updateToken(res.session_jwt); // same session, carry on
}
// If res.resumed is false the old session ended — start a fresh one.
});
session.start();
// Runs on unmount, and again between StrictMode's dev remount.
return () => {
session.stop();
session.destroy();
sessionRef.current = null;
};
// One attempt, one effect: identity comes from your backend, not re-renders.
}, [jwt, sessionId, attemptId, remint]);
return sessionRef;
}Two events earn a listener. The permission event reports whether the candidate granted the camera, so you can enforce your own policy. The token-expired event fires only if a token dies mid-attempt, so capture never fails silently. The default flow records as soon as the camera is granted and uses the first captured frame as the identity reference. If you want matching against a deliberate photo instead, create the session with autoStartCapture: false, call captureIdentity() after start() resolves, then call beginCapture().
The exam component just calls the hook. When the candidate submits, navigates away, or your router swaps the route, the component unmounts and React runs the cleanup you returned, which calls stop() then destroy(). That is the whole reason to lean on effect cleanup: it fires on every way out, so you never leave an enclave running.
// ProctoredExam.tsx (mark 'use client' in the Next.js App Router)
'use client';
import { useProctoring } from './useProctoring';
type Props = {
session: { jwt: string; id: string }; // fetched from your backend
attempt: { id: string }; // your own attempt record
};
export function ProctoredExam({ session, attempt }: Props) {
useProctoring({
jwt: session.jwt,
sessionId: session.id,
attemptId: attempt.id,
remint: (attempt_id) =>
fetch('/api/proctoring/session', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ attempt_id }),
}).then((r) => r.json()),
});
// The camera preview is the SDK's own floating enclave; render your exam freely.
return <ExamQuestions attempt={attempt} />;
}In React 18 StrictMode, development deliberately mounts, unmounts, and remounts the effect to surface missing cleanup. Because your cleanup fully tears the session down, the second mount starts cleanly rather than stacking a second camera preview. If your page sets a Content-Security-Policy, allow the enclave origin so the iframe can mount:
frame-src https://enclave.proctorlink.com;The session token is short-lived. For an exam that can outlast it, the token-expired handler in the hook re-mints on your backend for the same attempt_id. Because that attempt still has an active session, ProctorLink returns the existing session_id with a fresh token and resumed set to true, and you pass the new token to updateToken(). Events queued while the token was dead are flushed as soon as it lands, so nothing from the gap is lost.
Check resumed before calling updateToken(). A re-mint only resumes while the session is still active; if the abandonment sweep has already closed it, the mint creates a new session whose token belongs to a different attempt. The simplest way to avoid the whole situation is to set ttl_seconds generously at mint and treat token-expired as a safety net.
After the attempt, 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 a candidate cannot influence it from the React app no matter what they do in the browser. A score alone should not fail anyone: use reasons and evidence to drive human review of anything flagged.
useState. The session is an imperative handle, not render state. Putting it in state triggers re-renders and leaves you with stale closures when you later call updateToken(). Fix: hold it in a useRef and read sessionRef.current from your handlers.ProctorLink.createSession() inside a useEffect, and mark the component 'use client'.stop() then destroy(), StrictMode doubles the camera preview in development and, worse, real navigation leaves an enclave running. Fix: return the cleanup from useEffect.attempt_id on render. A value created with useState(() => crypto.randomUUID()) or on each mount defeats resume, so every refresh mints a new session and bills the attempt twice. Fix: derive attempt_id from your own attempt record so it is stable for the attempt.token-expired and call updateToken() with a freshly minted token for the same attempt_id.The split matters more in a single-page app, where it is tempting to keep everything in the client. Your API key is server-side only. The browser receives a short-lived, origin-bound session token, and that token lives inside the cross-origin iframe enclave served from ProctorLink’s own domain rather than in your React state or props. 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 from a React devtools console.
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 run inside Moodle instead of your own React app, you can skip this integration 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 proctor exams in your React app? Get credentials, wire the hook above into a real attempt, 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.
The framework-agnostic version of this guide, with the same three calls.
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.
A step-by-step guide to adding exam proctoring to a web app you own: mint a session on your backend, run the browser SDK, and read a server-side integrity report, with code from the API reference.
What browser-based proctoring can detect (tab switches, clipboard, faces, identity) and what it cannot (phones, a second monitor, a virtual machine), and why the tiering exists.
Your backend mints a session, a custom hook runs it inside an effect, and you read a server-side integrity score. Pilot the SDK on a real attempt and check the reviewer workload before you commit.