@beexpert.ai/proctor
v2.2.0
Published
BeExpert Proctoring SDK.
Readme
BeExpert Proctoring Client Guide
This guide is for client product and engineering teams integrating BeExpert Proctoring into assessment, interview, and certification flows.
Step 1: Install the Package
Install the SDK in your frontend project:
# Production registry
npm install @beexpert.ai/proctor
# OR local tarball
npm install ./beexpert.ai-proctor-<version>.tgzFor React apps, use the React integration:
import { useProctor } from '@beexpert.ai/proctor/react';Step 2: Provision a Session from Your Backend
Do NOT call the proctoring SDK first. Your frontend should call your own backend to provision a session.
2.1 Your Backend Calls Proctoring API
Your backend should provision the session with BeExpert API:
API call format:
POST /api/v1/proctoringSessions HTTP/1.1
Host: apiBaseUrl
Content-Type: application/json
x-api-key: <your-api-key>
x-tenant-id: <your-tenant-id>
x-user-id: SYSTEM
{
"EvaluationOn": {
"Type": "Assessment", // proctoring works on different Type but configured only for Assessment
"On_Id": "CHEM-NEET-2026", // Assessment On record Key like CHEMISTRY NEET (i.e. Name or unique identifier for the assessment)
"title": 'Chemistry NEET 2026 Assessment', // friendly title
scheduledStartAt: new Date().toISOString(),/ start of assessment
scheduledDurationSeconds: 900,// remaining time of assessment
},
"candidate": {
"candidate_id": "STU-10293", // candidate Key
"candidate_name": "Rahul Sharma",
"candidate_mobile": "1234567890",
"candidate_email": "[email protected]"
}
}Curl example:
curl -X POST "https://apiBaseUrl/api/v1/proctoringSessions" \
-H "Content-Type: application/json" \
-H "x-api-key: <your-api-key>" \
-H "x-tenant-id: <your-tenant-id>" \
-H "x-user-id: SYSTEM" \
-d '{
"EvaluationOn": {
"Type": "Assessment",
"On_Id": "CHEM-NEET-2026",
"title": "Chemistry NEET 2026 Assessment",
"scheduledDurationSeconds": 900,
"scheduledStartAt": "2026-07-03T06:19:10.678Z"
},
"candidate": {
"candidate_id": "STU-10293",
"candidate_name": "Rahul Sharma",
"candidate_mobile": "1234567890",
"candidate_email": "[email protected]"
}
}'Response from Backend:
{
"session": {
"id": "session-uuid-here"
},
"tokens": {
"accessToken": "v4.public.eyJ...",
"accessTokenTTL": 960
}
}2.2 Backend Returns Bootstrap Payload to Frontend
Your backend returns session credentials to the frontend:
{
sessionId: 'session-uuid-here',
sessionToken: 'v4.public.eyJ...',
accessTokenTTL: 960,
apiUrl: 'https://apiBaseUrl/api/v1',
scheduledStartAt: '2026-07-02T14:30:00Z',
scheduledDurationSeconds: 900,
title: 'Resume Screening Test'
}Key Points:
sessionId: Unique identifier for this assessment attemptsessionToken: JWT valid until (assessmentStart + assessmentDuration + 60 seconds)accessTokenTTL: Token validity duration in secondsscheduledStartAt: When the assessment is scheduled to begin (ISO 8601 timestamp)scheduledDurationSeconds: Total assessment time in secondsapiUrl: Backend proctoring API base URL including/api/v1(usehttps://apiBaseUrl/api/v1)
Step 3: Initialize the SDK with Session Credentials
3.1 Store Credentials in Session Storage
Once you receive the bootstrap payload, store it for the assessment page:
const sessionBootstrap = await response.json();
sessionStorage.setItem('bx_sessionId', sessionBootstrap.sessionId);
sessionStorage.setItem('bx_token', sessionBootstrap.sessionToken);
sessionStorage.setItem('bx_tokenExpirySeconds', String(sessionBootstrap.accessTokenTTL + Math.floor(Date.now() / 1000)));
sessionStorage.setItem('bx_title', sessionBootstrap.title);
sessionStorage.setItem('bx_scheduledStartAt', sessionBootstrap.scheduledStartAt);
sessionStorage.setItem('bx_scheduledDurationSeconds', String(sessionBootstrap.scheduledDurationSeconds));
sessionStorage.setItem('bx_apiBaseUrl', sessionBootstrap.apiUrl);
router.push('/assessment');3.2 Retrieve and Pass to useProctor
On your assessment page, retrieve credentials and initialize the SDK:
import { useProctor } from '@beexpert.ai/proctor/react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
export default function AssessmentPage() {
const [credentials, setCredentials] = useState(null);
useEffect(() => {
const sessionId = sessionStorage.getItem('bx_sessionId');
const sessionToken = sessionStorage.getItem('bx_token');
const apiUrl = sessionStorage.getItem('bx_apiBaseUrl');
if (!sessionId || !sessionToken) {
router.push('/'); // Redirect if no valid session
return;
}
setCredentials({ sessionId, sessionToken, apiUrl });
}, []);
// Initialize SDK with session credentials
const { start, stop, status, on } = useProctor({
sessionId: credentials?.sessionId,
sessionToken: credentials?.sessionToken,
apiUrl: credentials?.apiUrl,
autoStart: false, // User must click "Start" button
ui: {
enabled: true,
showProctoringControls: true,
toastDurationMs: 4000,
showWarningBanner: true, // to enable topbar message
showWarningToast: true // to enable alongside message
},
logLevel: 'debug'
});
const handleStartAssessment = async () => {
await start();
};
const handleStopAssessment = async () => {
await stop('CANDIDATE_ENDED');
};
return (
<div>
<button onClick={handleStartAssessment} disabled={status === 'running'}>
Start Proctored Assessment
</button>
<button onClick={handleStopAssessment} disabled={status !== 'running'}>
End Assessment
</button>
<div>Status: {status}</div>
</div>
);
}Step 4: Resume Assessment on Page Refresh
If the user refreshes the page or navigates away, you can resume the same session using the same token:
// On page refresh/reload
const sessionId = sessionStorage.getItem('bx_sessionId');
const sessionToken = sessionStorage.getItem('bx_token');
const apiUrl = sessionStorage.getItem('bx_apiBaseUrl');
if (sessionId && sessionToken) {
// Resume same session - SDK continues with the same token
const { start, stop, status } = useProctor({
sessionId,
sessionToken,
apiUrl,
autoStart: false
});
// SDK will resume from where it left off
await start();
}Important:
- The same
sessionTokenis valid for the entire assessment duration - No token refresh needed - token is valid until assessment ends + 60 seconds
- Store
sessionIdandsessionTokenin session storage to survive page refresh - If page is refreshed, use the exact same credentials to resume
Step 5: Understanding Assessment Timing
5.1 Scheduled Start Time vs. Actual Start Time
The assessment has a scheduled window, but the candidate may start late:
Scenario: Assessment window is 2:00 PM - 5:00 PM (3 hours)
Candidate joins at: 2:30 PM
scheduledStartAt: 2:00 PM (original window start)
scheduledDurationSeconds: 10,800 (3 hours = 180 minutes)
BUT the candidate only has time from 2:30 PM onward.
Total remaining time: 2.5 hours = 9,000 seconds5.2 Backend Calculates Actual Assessment Time
Your backend should provision the session with actual timing:
// Backend logic when candidate joins at 2:30 PM
const windowStart = new Date('2026-07-02T14:00:00Z');
const windowEnd = new Date('2026-07-02T17:00:00Z');
const candidateStartTime = new Date('2026-07-02T14:30:00Z');
// For provisioning, use actual times
const scheduledStartAt = Actual time; // 2:00 PM
const actualDuration = (windowEnd - candidateStartTime) / 1000; // 9,000 seconds
// Token valid until: assessmentStart + assessmentDuration + 60 seconds
// 2:30 PM + 2.5 hours + 1 min = 5:01 PM5.3 Token Expiry Calculation
The SDK calculates token expiry as on start proctoring:
Token Expiry = candiateStartTime + scheduledDurationSeconds + 60 secondsExample:
- Proctoring Start: 2:30 PM
- Duration: 2.5 hours (9,000 seconds)
- Token Expires: 2:30 PM + 2.5 hours + 1 minute = 5:01 PM
If the candidate is still in the assessment at 5:01 PM, the token will be expired and the session will terminate.
6. Session Lifecycle and Status
The SDK lifecycle is:
- Initialize:
useProctor()with credentials - Start:
await start()- requests permissions, starts monitors/transport - Run: violations and warnings are evaluated continuously
- Stop:
await stop(reason)- finalizes and ends session
Current status values you can receive on session:status:
idleinitializingreadyrunningpausedstoppingterminatedcompletederror
Recommended UI behavior:
- Show full-screen blocking spinner only during
initializing. - During
stopping, disable Start/Stop buttons and show "Ending assessment...". - Treat
terminatedandcompletedas terminal states.
7. Events to Handle (Important)
The SDK provides a comprehensive event bus that emits strongly-typed events during the assessment. You can listen to these events to drive your own custom UI or telemetry.
7.1 Lifecycle Events
// Session state transitions
on('session:status', ({ status, state }) => {
console.log('Status changed:', status, state);
});
// Emitted when SDK has fetched config and is ready to start
on('session:ready', ({ state }) => {
console.log('Proctoring ready to start');
});
// Emitted when session actually starts.
// CRITICAL: Save the new sessionToken to survive network calls!
on('session:started', ({ state, sessionToken }) => {
console.log('Proctoring started');
if (sessionToken) {
sessionStorage.setItem('bx_token', sessionToken);
}
});
// Terminal states
on('session:terminated', ({ reason, state, serverResponse }) => {
console.log('Session forcibly terminated:', reason);
// Note: Because this fires instantly to freeze the UI, serverResponse is undefined here.
// To get the server's response for an abandoned session, await proctor.stop(reason).
});
on('session:completed', ({ state, summary, serverResponse }) => {
console.log('Assessment naturally completed', summary);
console.log('Server response:', serverResponse);
});
// Unrecoverable errors
on('session:error', ({ error }) => {
console.error('Proctoring error:', error);
});7.2 Violation & Detection Events
// Raw violation detected (before debouncing)
on('violation:raw', ({ violation }) => {
console.log('Raw violation:', violation.type);
});
// Confirmed violation (after debouncing)
on('violation:confirmed', ({ violation }) => {
console.log('Violation confirmed:', violation.type, violation.reason);
});
// Canonical backend event mapping (best for custom UI warnings)
on('detection', ({ type, severity, eventType, violation }) => {
console.log('Detection Event:', eventType, type, severity);
});
// Accumulation of warnings (drives termination logic)
on('violation:warning', ({ violation, mediumCount, maxMedium, highCount, maxHigh }) => {
console.log(`Warnings: Medium(${mediumCount}/${maxMedium}) High(${highCount}/${maxHigh})`);
});
// Server-aligned escalation (WARN, BLOCK, CANCEL)
on('violation:escalation', ({ action, eventType, occurrenceCount, windowSeconds, violation }) => {
console.log(`Escalation: ${action} for ${eventType} (${occurrenceCount} times in ${windowSeconds}s)`);
});7.3 Media, Transport, and UI Events
// Camera/Mic/Screen permission states
on('permission:request', ({ kind }) => console.log('Requesting:', kind));
on('permission:change', ({ kind, state }) => console.log('Permission changed:', kind, state));
// Media streams status
on('media:stream', ({ kind, active }) => console.log('Media:', kind, active));
// Recording events
on('recording:chunk', ({ index, size }) => console.log(`Recording slice ${index} captured (${size} bytes)`));
on('recording:chunk:uploaded', ({ index, chunk, url }) => {
console.log(`Slice ${index} successfully uploaded to ${url}`);
// You can stream the raw Blob (chunk) locally without re-downloading it
});
// Transport connectivity
on('transport:open', () => console.log('WebSocket open'));
on('transport:close', ({ code, reason }) => console.log('WebSocket closed:', code, reason));
on('transport:reconnecting', ({ attempt, delayMs }) => console.log(`Reconnecting: attempt ${attempt} in ${delayMs}ms`));
// Internal SDK Toasts & Dialogs
on('ui:toast', ({ message, severity, durationMs }) => console.log('Toast:', severity, message));
on('ui:dialog', ({ kind, payload }) => console.log('Dialog opened:', kind));7.4 All Available Violation Types
The SDK detects a wide array of anomalies. These are categorized into AI/Vision, Audio, Browser, Media, and System violations:
AI / Vision:
NO_FACEMULTIPLE_FACESFACE_MISSINGLOOKING_AWAYEYES_CLOSEDHEAD_POSE_OUT_OF_RANGEFACE_TOO_FARMOBILE_PHONE_DETECTEDEXTRA_PERSON_DETECTEDVIRTUAL_CAMERA_DETECTED
Audio:
BACKGROUND_NOISEMULTIPLE_VOICESVOICE_CHANGED
Browser:
TAB_SWITCHWINDOW_BLURFULLSCREEN_EXITDEVTOOLS_OPENCOPY_DETECTEDPASTE_DETECTEDCONTEXT_MENU_BLOCKED
Media / System:
CAMERA_LOSTMICROPHONE_LOSTSCREEN_SHARE_STOPPEDSCREEN_CHANGEPERMISSION_DENIEDNETWORK_DISCONNECT
7.5 Violation Event Meaning
violation:confirmed: a rule condition was confirmed by detector logic.violation:warning: warning counters were updated.violation:escalation: escalation action computed by engine (WARN,BLOCK,CANCEL).
confirmed does not always mean session ends. Session ends when threshold/escalation logic decides so.
7.6 Typical Event Order (Normal Candidate Stop)
session:status -> running- user clicks Stop
session:status -> stopping- server finalize (
completefor candidate-ended, otherwiseabandon) session:completed(candidate-ended flow)
7.7 Typical Event Order (Threshold Termination)
- repeated
violation:confirmed violation:warningreaches thresholdsession:terminatedwith reason such asMAX_WARNINGS_EXCEEDEDsession:statusreaches terminal state
For terminated flow, do not expect session:completed.
7.8 About 409 Near Session End
A single 409 around shutdown can occur if an in-flight request races with end-of-session. The SDK treats terminal statuses as shutdown signals and closes the loop. If termination/completion event already arrived, treat this as expected end noise.
8. What You Must Know
- No Token Refresh: Token is valid for entire assessment. No refresh endpoint.
- Session Storage: Store
sessionId,sessionToken, and timing in session storage. - Tenant Isolation: SDK enforces tenant validation from token claims.
- Assessment Timing: Backend must provision with actual candidate times, not window times.
- Single Token: One JWT token valid from assessment start to assessment end + 60 seconds.
- No Manual Expiry Handling: SDK automatically handles token expiry.
- Terminal UX Rule: Once
session:terminatedorsession:completedarrives, lock assessment UI and show end-state screen.
9. Fetching the Session Report
9.1 SDK Internal Session Completion
When the session is completed or abandoned gracefully by the SDK (e.g. candidate ends the session), the SDK internally makes a request to /complete.
- Endpoint:
POST /api/v1/proctoringSessions/:sessionId/complete - Headers:
Authorization: Bearer <sessionToken> - Body:
{ "reason": "CANDIDATE_ENDED" }
The backend finalizes the session, triggers background server-side analysis if configured, and returns the Unified Session Report. Because this endpoint directly returns the final report, your client-side application can immediately use this response data to display a results summary (like an integrity score) right as the session concludes.
[!TIP] Implementation Best Practice: If your assessment runs on a timer, it is highly recommended to call
proctor.stop()exactly 2-5 seconds before your assessment timer reaches zero. This gives the SDK the necessary time to flush the final local violations, request the/completereport from the server, and receive the calculated integrity score precisely as the assessment ends, resulting in a seamless UX without loading spinners.
9.2 Fetching the Report Later (Server-to-Server)
To retrieve the session report later (e.g., from your backend server to display to the examiner/reviewer), make a GET request using your API key.
Endpoint:
GET /api/v1/proctoringSessions/reports/:sessionId HTTP/1.1
Host: apiBaseUrl
x-api-key: <your-api-key>
x-tenant-id: <your-tenant-id>
x-user-id: SYSTEMCurl Example:
curl -X GET "https://apiBaseUrl/api/v1/proctoringSessions/reports/session-uuid-here" \
-H "x-api-key: <your-api-key>" \
-H "x-tenant-id: <your-tenant-id>" \
-H "x-user-id: SYSTEM"9.3 Unified Report Response Schema
Both the SDK's complete call and the Server's report fetch return the exact same ProctoringSessionReportResponse schema.
Response Body (200 OK):
{
"sessionId": "6a4c86ac0e6682736f8a55c0",
"summary": {
"candidateId": "STU-10293",
"sessionStatus": "COMPLETED",
"startedAt": "2026-07-02T14:30:00Z",
"endedAt": "2026-07-02T15:00:00Z",
"durationSeconds": 1800,
"integrityScore": 85,
"totalViolations": 2
},
"violations": [
{
"id": "6a4c86ac0e6682736f8a55c8",
"violationType": "THRESHOLD_BREACH",
"eventType": "Window Minimized",
"capturedAt": "2026-07-07T04:55:06.882Z",
"timestamp": "2026-07-07T04:55:06.882Z",
"severity": "HIGH",
"evidenceUrl": "https://apiBaseUrl/presigned-url-to-evidence.webp",
"evidenceIds": ["6a4c86ac0e6682736f8a55cb"]
}
],
"recordings": []
}Property Breakdown:
Top-Level Properties:
sessionId(string): Unique identifier for the proctoring session.summary(object): Overview metrics of the entire session.violations(array): List of captured infractions or anomalies.recordings(array): References to full video/screen recordings if continuous recording was enabled.
Summary Properties:
candidateId(string): The ID of the candidate provided during session provisioning.sessionStatus(string): The terminal state of the session (e.g.,COMPLETED,TERMINATED,ABANDONED).startedAt(string, ISO-8601): When the session physically began streaming telemetry.endedAt(string, ISO-8601): When the session physically stopped.durationSeconds(number): Total active proctoring time.integrityScore(number): Calculated trustworthiness score (0-100) based on severity of violations.totalViolations(number): Total count of violations triggered during the session.
Violation Properties:
id(string): Unique identifier of the specific violation occurrence.violationType(string): The underlying raw classification metric (e.g.,THRESHOLD_BREACH,FACE_MISSING).eventType(string): Human-readable name of the rule that was breached (e.g., "Window Minimized", "Multiple Faces Detected").severity(string): The severity level assigned to the rule (LOW,MEDIUM,HIGH,CRITICAL).capturedAt/timestamp(string, ISO-8601): Time when the violation was first detected.evidenceUrl(string): Expiring pre-signed URL to view the snapshot/video evidence of the violation.evidenceIds(array): Internal reference IDs mapping to the underlying evidence records.
