SDK Reference
One factory, one session object, and a set of events you subscribe to. The SDK handles token refresh, batching, retries and reconnection on its own.
Install
npm install @incproctor/proctoring-sdkyarn add @incproctor/proctoring-sdkpnpm add @incproctor/proctoring-sdk<script src="https://cdn.jsdelivr.net/npm/@incproctor/proctoring-sdk"></script>
<!-- exposes window.InCruiterProctoring -->The package ships TypeScript types. Named exports only — there is no default export.
import {
createProctoringSession, // the factory you call
ProctoringSession, // the class it returns
ProctoringEvent, // event name constants
ProctoringError, // the error type
checkSupport, // browser capability probe
VERSION,
} from '@incproctor/proctoring-sdk'
Lifecycle
Four steps, and only the first happens on your server.
-
Your backend mints a session
Your server calls the mint endpoint and receives
session_idandsession_token. See the quickstart.Your API key and secret never enter the browser. Only the minted session belongs client-side — it expires, and it works for one candidate on one origin.
-
Create the session in the browser
Creating it does not start monitoring or request any permission.
const proctoring = createProctoringSession({ baseUrl: INCPROC_BASE, // the same base URL your server uses session, // exactly what your backend received camera: { enabled: true, required: false }, evidence: { enabled: true, intervalSec: 15, sources: ['camera'], maxDimension: 640 }, }) -
Start it
start()is where permissions are requested and monitoring begins. Call it from a user gesture — browsers refuse camera access otherwise.const status = await proctoring.start() console.log(status.state) // 'active' -
Stop it when the candidate finishes
Flushes anything queued and closes the session cleanly.
await proctoring.stop()If the tab simply closes, the session is detected and closed server-side — but the explicit call is cleaner and settles the credit sooner.
Browser support
Check before you create a session, so you can tell the candidate what will not work before the exam rather than during it.
import { checkSupport, UNSUPPORTED_DETECTIONS } from '@incproctor/proctoring-sdk'
const support = checkSupport()
if (!support.supported) {
// Tell the candidate to switch browser before they begin.
}
The SDK requires a modern browser with getUserMedia, fetch and the
Page Visibility API. It runs only in a secure context — HTTPS, or
localhost. On plain http:// the browser blocks camera access
outright.
Configuration
createProctoringSession(config)
| Option | Type | Notes |
|---|---|---|
baseUrl | string | Required. The base URL issued to you. No trailing path. |
session | object | Required. Exactly what your backend received from the mint endpoint. |
camera | CameraOptions | See below. Enabled unless you set enabled: false. |
microphone | MicrophoneOptions | Same shape as camera. Off unless you set enabled: true. |
screen | ScreenShareOptions | Screen sharing. The candidate chooses what to share; the SDK reports which. |
evidence | EvidenceOptions | Periodic stills. Off unless you set enabled: true. |
monitors | MonitorOptions | Which browser activity to watch. See the defaults below — they are not all the same. |
environment | EnvironmentOptions | One system/device snapshot at start(). On by default. |
flushIntervalSec | number | How often the queue is drained. Default 5, minimum 1. High-severity events flush immediately regardless. |
maxQueueSize | number | Cap on unsent events held in memory; past it the oldest are dropped. Default 500. |
heartbeatSec | number | Liveness ping interval. Default 60, minimum 30. |
logLevel | string | warn (default), error, debug, silent. |
fetchImpl | function | Custom fetch, for test harnesses and instrumented clients. |
The SDK never logs the session token, a media frame, or event detail at any
log level. debug adds timing and transport information only.
session — SessionCredentials
Pass the mint response through unchanged. The field names are the API's, in
snake_case; do not rename them to camelCase on the way.
| Field | Type | Notes |
|---|---|---|
session_id | string | Required. |
session_token | string | Required. Scoped to this one session. Never send your API key to the browser. |
token_expires_at | string | ISO 8601. |
session_expires_at | string | ISO 8601. After this the session is expired and cannot be resumed. |
capabilities | string[] | What this session is permitted to do. |
reference_id | string | null | Your own identifier, echoed back on the report. |
ingest_base_url | string | null | Where events go, when it differs from baseUrl. |
camera / microphone — CameraOptions, MicrophoneOptions
| Field | Type | Default | Notes |
|---|---|---|---|
enabled | boolean | camera true, microphone false | The two differ. Set microphone.enabled: true explicitly if you want audio. |
required | boolean | false | true makes start() reject if the candidate denies the permission. |
constraints | MediaTrackConstraints | — | Standard DOM constraints, passed to getUserMedia as-is. |
screen — ScreenShareOptions
start() never asks for screen sharing. You must call
requestScreenShare() yourself, from a click handler. Every browser
requires a user gesture for getDisplayMedia, and start() can be
called from anywhere, so the SDK cannot do it for you. Configuring screen and
never calling requestScreenShare() means screen sharing simply never happens —
with no error. This is the single most common integration mistake with this SDK.
| Field | Type | Default | Notes |
|---|---|---|---|
enabled | boolean | — | Has no effect. It exists on the type, but screen sharing starts only via requestScreenShare(). Setting it does not start anything. |
required | boolean | false | true makes requestScreenShare() throw if the candidate refuses, instead of resolving false. |
displaySurface | 'monitor' | 'window' | 'browser' | 'monitor' | Which surface the picker offers first. A hint — the candidate still chooses. |
preferCurrentTab | boolean | — | Set false to exclude the exam's own tab from the picker, pushing the candidate toward the whole screen. Chromium-only; ignored elsewhere. Any other value does nothing. |
You cannot force a candidate to share their whole screen, and you cannot stop them
pressing the browser's own "Stop sharing" button. Both are browser-enforced. Read
getStatus().screen.displaySurface to see what they actually picked, and handle
SCREEN_SHARE_STOPPED rather than assuming sharing lasts the whole exam.
evidence — EvidenceOptions
| Field | Type | Default | Notes |
|---|---|---|---|
enabled | boolean | false | Must be true for any still to be captured. |
intervalSec | number | 30 | Seconds between periodic stills. |
sources | ('screen' | 'camera')[] | screen if sharing, else camera | What to capture from. |
maxDimension | number | 1280 | Longest edge in pixels; the still is scaled down to fit. |
quality | number | 0.6 | JPEG quality, 0–1. |
captureOnEvent | boolean | true | Also capture a still when a significant event fires, not only on the timer. |
monitors — MonitorOptions
| Field | Default | Watches |
|---|---|---|
activity | on | Tab switches, window focus, page unload. |
fullscreen | on | Entering and leaving fullscreen. |
network | on | Connection lost and restored. |
devices | on | Cameras and microphones attached or removed. |
clipboard | off | Copy, paste and right-click. Opt in with clipboard: true. |
print | off | Print attempts. Needs clipboard: true as well — it is carried by the same monitor. |
Clipboard and print are off by default on purpose. They are the most privacy-sensitive signals the SDK can collect, so turning them on is your decision to make and to disclose to candidates — not a default they inherit.
environment — EnvironmentOptions
| Field | Type | Default | Notes |
|---|---|---|---|
enabled | boolean | true | The one-off OS / browser / screen / device-count snapshot at start(). |
deviceLabels | boolean | true | Include human-readable device names. Set false to record counts only. |
To switch the snapshot off, write environment: { enabled: false } —
an object, not a boolean. environment: false does not disable it; the SDK
reads environment.enabled, and a boolean has no such property, so the snapshot
still fires.
Methods
Lifecycle
| Method | Returns | What it does |
|---|---|---|
start() | Promise<ProctoringStatus> | Requests camera/microphone permissions and begins monitoring. Call from a user gesture. Does not request screen sharing or fullscreen. |
stop(reason?) | Promise<void> | Flushes the queue and ends the session. reason defaults to 'completed'. Terminal — a stopped session cannot be restarted. |
pause() | Promise<void> | Suspends capture. Events still record — a paused session still explains why it was paused. |
resume() | Promise<void> | Resumes capture. |
destroy() | void | Releases media tracks and listeners. Call on unmount. Does not end the session server-side — call stop() first. |
pause() suppresses capture, not the timeline. If a candidate is
allowed a break, pause rather than stop — stopping ends the session and it cannot be resumed.
Permissions you must ask for yourself
These need a user gesture — call them from a click or key-press handler, never
on page load or inside a useEffect. The browser silently refuses otherwise.
| Method | Returns | What it does |
|---|---|---|
requestScreenShare() | Promise<boolean> | Opens the screen picker. false if the candidate refuses — or throws, if screen.required is set. |
requestFullscreen(element?) | Promise<boolean> | Puts the page into fullscreen. Defaults to the whole document. |
requestMicrophone() | Promise<boolean> | Starts the microphone later, if you did not enable it at start(). |
Evidence & media
| Method | Returns | What it does |
|---|---|---|
captureEvidence(reason?) | Promise<void> | Takes one still now, outside the schedule. reason defaults to 'manual'. Throws INVALID_CONFIG if evidence.enabled is not set. |
getCameraStream() | MediaStream | undefined | The live camera stream, for showing the candidate their own preview. |
Events & state
| Method | Returns | What it does |
|---|---|---|
on(name, handler) | Unsubscribe | Subscribe. Returns a function that removes the handler. |
once(name, handler) | Unsubscribe | Subscribe for one firing only, then auto-unsubscribe. |
off(name, handler?) | void | Unsubscribe. Omit the handler to remove all for that event. |
onError(handler) | Unsubscribe | Shorthand for PROCTORING_ERROR. The handler receives (error, payload) with the ProctoringError already unwrapped. |
reportCustomEvent(type, detail?) | void | Record your own event on the session timeline — "question 4 opened", "calculator used". See the note below. |
getStatus() | ProctoringStatus | The full current state, synchronously. |
sessionState | SessionState | Getter. Just the state string, when you do not need the whole status object. |
requestScreenShare(), requestMicrophone() and
captureEvidence() throw if the session is not running. Check
sessionState === 'active' first if you call them from UI that can outlive the
session. requestFullscreen() does not check, because fullscreen is a property of
your page rather than of the session.
Custom events are deliberately constrained. type is prefixed
with app., lower-cased to a–z 0–9 _ and truncated to 48 characters, so
your event can never collide with or impersonate one of the SDK's own. The server accepts
unknown types — an older gateway must not reject a newer SDK — but always scores them
low severity. You cannot raise a candidate's risk score from the browser, by
design. reportCustomEvent also does nothing at all unless the session is
active or paused; it returns silently rather than throwing.
Events
Subscribe with the constant, never a raw string — the constants are what your editor autocompletes and your compiler checks.
import { ProctoringEvent } from '@incproctor/proctoring-sdk'
const off = proctoring.on(ProctoringEvent.TAB_SWITCH, (payload) => {
showWarning('Please stay on the exam tab.')
})
// Everything, for logging or a live activity feed:
proctoring.on(ProctoringEvent.ANY, (payload) => console.debug(payload))
off() // unsubscribe
Activity
| Constant | Fires when |
|---|---|
TAB_SWITCH | The exam stopped being the visible tab. The core signal. |
TAB_RETURN | They came back. Carries how long they were away. |
WINDOW_BLUR | Focus lost, but the page may still be visible — a second monitor, a notification. |
WINDOW_FOCUS | Focus returned. |
FULLSCREEN_ENTER / FULLSCREEN_EXIT | Fullscreen changed. |
PAGE_UNLOAD | The page is closing. A final flush is attempted. |
COPY / PASTE / CONTEXT_MENU / PRINT | The corresponding action occurred. |
Blur and tab-switch are scored differently on purpose. A candidate on two monitors generates blur constantly without ever leaving the exam. Treating that as "left the test" would flag every one of them.
Media
| Constant | Fires when |
|---|---|
CAMERA_STARTED / CAMERA_STOPPED | The camera track began or ended. |
CAMERA_INTERRUPTED | The track died unexpectedly — unplugged, taken by another app. |
MICROPHONE_STARTED / _STOPPED / _INTERRUPTED | The same, for audio. |
SCREEN_SHARE_STARTED / _STOPPED / _INTERRUPTED | The same, for screen sharing. |
DEVICES_CHANGED | A camera or microphone was attached or removed. |
PERMISSION_DENIED | The candidate refused a permission. |
Session & connection
| Constant | Fires when |
|---|---|
SESSION_STARTED / _PAUSED / _RESUMED / _STOPPED | The session changed state. |
SESSION_EXPIRED | The session outlived its TTL. Monitoring has stopped — mint a new one to continue. |
CONNECTION_LOST / CONNECTION_RESTORED | The network dropped or returned. Events queue meanwhile and are resent. |
ENVIRONMENT_REPORTED | The one-off system snapshot was sent. |
EVIDENCE_CAPTURED / EVIDENCE_UPLOADED | A still was taken, then stored. |
PROCTORING_ERROR | Something failed. Carries a ProctoringError. |
ANY | Wildcard — receives every event above. |
These are advisory signals, not proof. They are produced by JavaScript on the candidate's own machine and can be suppressed or fabricated. Use them to route an interview to a human reviewer — never to fail a candidate automatically. See What a finding means.
Status & state
const status = proctoring.getStatus()
// {
// state: 'idle' | 'starting' | 'active' | 'paused' | 'stopping' | 'stopped' | 'expired',
// sessionId: 'pbs_…',
// referenceId: 'your-own-id' | null,
// connection: 'online' | 'offline' | 'degraded',
// elapsedSec: 412,
//
// camera: { active: true, permission: 'granted', deviceLabel: 'FaceTime HD Camera' },
// microphone: { active: false, permission: 'prompt' },
// screen: { active: true, permission: 'granted', displaySurface: 'monitor' },
//
// events: { generated: 37, sent: 35, queued: 2, dropped: 0 },
// evidence: { captured: 12, uploaded: 12, failed: 0 },
//
// hidden: false, // is the exam tab currently hidden?
// fullscreen: true,
// }
The three media entries are MediaStatus objects, each carrying
active, permission
(granted | denied | prompt | unknown),
an optional deviceLabel, an error string when something went wrong, and —
for screen only — displaySurface, which is what the candidate actually chose to
share.
sessionState is a shorthand getter returning just the state string.
dropped is the number you should alert on. Events are dropped
only when the queue hit maxQueueSize while offline — meaning some of this session's
activity was never recorded and never will be. A non-zero dropped is a reason to
treat the report as incomplete, not merely a performance note.
Errors
Everything the SDK throws is a ProctoringError carrying a stable
code. Branch on the code, never the message — messages get
reworded, codes do not.
import { ProctoringError } from '@incproctor/proctoring-sdk'
try {
await proctoring.start()
} catch (err) {
if (err instanceof ProctoringError) {
console.error(err.code, err.message, err.requestId)
}
}
What a ProctoringError carries
| Property | Type | Notes |
|---|---|---|
code | ProctoringErrorCode | Stable. Branch on this. |
message | string | Human-readable. Never branch on it — it gets reworded. |
recoverable | boolean | Is retrying meaningful? false for a denied permission or an ended session. |
requestId | string | Quote this in a support ticket. It locates the exact server-side log for this failure. Present whenever the error came from an API response. |
serverCode | string | The API's own error code, when this error came from a response. Finer-grained than code. |
context | string | What the SDK was doing, e.g. 'camera.start'. |
reachedServer | boolean | Did the request arrive at all? See below. |
cause | unknown | The underlying DOMException or TypeError, kept for diagnosis. Never logged by the SDK. |
reachedServer: false means you learned nothing about your request —
only about the network. Offline, DNS failure, connection refused, timeout. Retrying is
reasonable. true means the server saw it and rejected it, so retrying the same
request unchanged will fail the same way. It is left undefined for errors that
never involved a request, such as a denied permission or a bad config.
Log requestId on every failure. It is the difference between a
support conversation that starts with "this exact request, here is what the server did" and one
that starts with "some time yesterday afternoon".
Every code
Grouped by who has to do something about it.
Your integration — fix these in code
| Code | Means |
|---|---|
INVALID_CONFIG | A required option is missing or malformed. Usually baseUrl or a session that was not passed through verbatim. |
ALREADY_INITIALIZED | start() called on a session that is already running. |
NOT_STARTED | A method that needs a live session was called before start(). |
SESSION_ALREADY_STOPPED | The session was stopped. Create a new one — a stopped session cannot restart. |
The candidate's environment — tell them what to change
| Code | Means |
|---|---|
BROWSER_UNSUPPORTED | Missing an API the SDK needs. Call checkSupport() first and steer them to a supported browser. |
INSECURE_CONTEXT | The page is plain http://. Camera access is impossible — serve over HTTPS. |
FEATURE_UNSUPPORTED | One capability is unavailable here, e.g. screen share on some mobile browsers. |
Permissions and devices — recoverable by the candidate
| Code | Means | Do |
|---|---|---|
PERMISSION_DENIED | They refused, or the browser has a stored block. | Explain how to re-enable it in the address bar. |
PERMISSION_DISMISSED | They closed the prompt without answering. | Offer a retry button — this one is just a re-ask. |
DEVICE_NOT_FOUND | No camera or microphone is attached. | Ask them to connect one and retry. |
DEVICE_IN_USE | Another application holds the device. | Ask them to close the video call and retry. |
Media
| Code | Means |
|---|---|
MEDIA_FAILED | A track could not be acquired or died unexpectedly. |
CAPTURE_FAILED | An evidence still could not be taken. Monitoring continues. |
Transport and session
| Code | Means | Do |
|---|---|---|
NETWORK_ERROR | A request could not be delivered. | Nothing — events queue and resend. Show a reconnecting banner. |
UNAUTHORISED | The token was rejected. Most often an origin mismatch. | Check origin at mint matches the page exactly. |
SESSION_EXPIRED | Past its TTL. Monitoring has stopped. | Mint a fresh session and start again. |
SESSION_ENDED | Ended from your backend while the browser was still running. | Treat as final. |
RATE_LIMITED | The per-session event or evidence ceiling was hit. | Nothing — the SDK backs off. See limits. |
SERVER_ERROR | Our side failed. | The SDK retries. Persisting means an incident — quote the time. |
UPLOAD_FAILED | An evidence image could not be stored. The event itself was recorded. | Nothing. |
The single most common failure is UNAUTHORISED from an origin
mismatch — the session was minted for a different origin than the page it runs on. The
token is bound to one origin and refused anywhere else, and the message does not say so.
https://exams.example.com and https://exams.example.com/ are the same;
a different subdomain, port or scheme is not.
Permissions
Three rules decide whether camera access will work at all, and none of them are ours:
| Secure context | HTTPS, or localhost. Plain http:// is blocked by the browser. |
| User gesture | Call start() from a click. A prompt on page load is suppressed by most browsers. |
| Iframe permissions | If your exam runs in an iframe, it needs allow="camera; microphone; display-capture" — otherwise access is denied with no prompt shown. |
Decide what a denied camera means to you. With
required: false the session continues and the refusal is recorded as a finding.
With required: true, start() rejects and you must handle it.
Most customers use false and let a reviewer judge.
Patterns
React
useEffect(() => {
let session
let cancelled = false
createExamSession().then((minted) => { // your backend
if (cancelled) return
session = createProctoringSession({ baseUrl: INCPROC_BASE, session: minted })
session.on(ProctoringEvent.TAB_SWITCH, () => setWarning(true))
return session.start()
})
return () => {
cancelled = true
// destroy() releases camera tracks — without it the light stays on.
session?.destroy()
}
}, [])
Warn, don't punish
let awayCount = 0
proctoring.on(ProctoringEvent.TAB_SWITCH, () => {
awayCount += 1
if (awayCount === 1) showBanner('Please stay on this tab.')
if (awayCount === 3) showBanner('Repeated tab switching is recorded in your report.')
// Do NOT end the exam here. The signal is advisory.
})
Survive a dropped network
proctoring.on(ProctoringEvent.CONNECTION_LOST, () => setBanner('Reconnecting…'))
proctoring.on(ProctoringEvent.CONNECTION_RESTORED, () => setBanner(null))
Nothing else is needed — events queue while offline and are resent on reconnect. Delivery is at-least-once and de-duplicated server-side.
Handle an expired session
proctoring.on(ProctoringEvent.SESSION_EXPIRED, async () => {
// Monitoring has stopped. Mint a fresh session on your backend and start again.
const minted = await createExamSession()
proctoring = createProctoringSession({ baseUrl: INCPROC_BASE, session: minted })
await proctoring.start()
})