The SDK sends structured events to the Proctor backend. Events are queued in IndexedDB, assigned idempotent IDs, and uploaded continuously during the session — the uploader drains the queue roughly every 2 seconds in batches of up to 50 events, with retry and backoff on failure. A clean session end performs a final drain, and pagehide triggers a keepalive flush so events survive a tab close or navigation.
session.ended is the clean path after media drain — the SDK emits it with reason: "stopped" when the host calls stop(). A candidate who simply disappears (disconnect, tab close, another device taking over) never gets to send it, so abandonment is recorded server-side: when a within-session attempt is retired without a clean session.ended, the server synthesises a session.abandoned timeline event marking the close of that attempt; a resume opens the next one with . ( also exists as a separate type, fired when the whole sitting ends abandoned — see .)
session.resumed fires when a candidate returns to a live attempt (a refresh, or reopening the tab). Because a two-second refresh and a twenty-minute absence take the same code path, the event reports how long the session was unobserved so you can tell them apart:
awayMs — the dark-gap in milliseconds (null if the leave time couldn't be recorded, e.g. storage disabled).
class — "refresh" (< 5 s) · "brief" (5 s–2 m) · "interruption" (2 m–10 m) · "returned-after-absence" (> 10 m) · "unknown". A long absence is the integrity signal — the candidate left the proctored environment — and the dashboard timeline badges it in red.
navigationType — the browser's own load classification ("reload" | "back_forward" | "navigate" | …) as a corroborator. Note it can't identify the refresh button specifically; "reload" also covers F5 and location.reload().
checks / changedDevices — the resume re-verification verdicts and any device the candidate switched.
The leave time is measured client-side (persisted to localStorage on pagehide/tab-hide, so it survives a full tab close). A hard crash that never fires pagehide falls back to class: "unknown".
Runtime events include focus/visibility changes, fullscreen exits, clipboard events, keyboard shortcuts, screen-share status, webcam status, media failures, permission denials, upload failures, worker failures, drain timeouts, and — when you run the runtime checkpoint — session.checkpoint audit records.
Events carry SDK timestamps assigned before queueing. The dashboard sorts by event timestamp, not ingest order, so delayed uploads still appear in chronological order.
The dashboard groups timeline events by category while preserving raw queryable event lines. Customer webhooks intentionally expose a smaller lifecycle subset.
Every observer emits typed events to the same ingest endpoint. Each event has a stable id, a monotonic timestamp, and an optional payload. Every event also carries a top-level fingerprintId — the per-device fingerprint stamped by the SDK at construction time — so the dashboard can render multi-device timelines as parallel swim lanes. The dashboard groups events by category, but every line below is queryable raw. Customer webhooks intentionally expose a smaller lifecycle subset; see the webhooks section for that delivery contract.
The tables below index every event kind. The sdk.* telemetry events share a common payload envelope — code, phase, recoverable, sdkVersion, buildSha, browser (platform / language / timezone / screen summary), networkOnline, visibilityState, and pageOrigin — plus the event-specific fields listed per row.
First accepted batch for a newly resolved internal session (server-stamped bookend).
—
session.resumed
Candidate returned to a live attempt after a refresh or tab close; see the lifecycle section above.
awayMs · class · navigationType · checks · changedDevices
session.abandoned
Server-derived close of an attempt that never sent a clean session.ended — the candidate dropped rather than finishing.
reason: string
session.heartbeat
Activity ping while the SDK session is live.
intervalMs: number · visibilityState?: string
session.fingerprint
One-shot device + browser snapshot stamped near the head of the timeline for continuity checks.
Full fingerprint detail: userAgent · platform · language(s) · timezone · timezoneOffset · screen · devicePixelRatio · hardwareConcurrency · deviceMemory · cookieEnabled · webdriver · plugins · userAgentData. (The short fingerprintId is a top-level field on every event, not this payload.)
session.policy
The complete resolved assessment policy applied to this SDK instance.
policy: object
session.end_requested
Keepalive marker that a clean stop began — sent before the media drain so the server sees intent even if the drain is cut short.
reason: "stopped" | "abandoned" · drainDeadlineMs: number
session.ended
Terminal SDK event. Clean stops trigger analysis; abandoned stops do not. The SDK's clean-stop path always sends reason: "stopped".
byteLength? · targetTag? · blocked? — plus content? · truncated? when content capture is enabled
clipboard.cut
Candidate cut selection.
Same shape as clipboard.copy
clipboard.paste
Candidate pasted into a field.
Same shape as clipboard.copy
contextmenu.opened
Right-click menu opened.
—
keyboard.blocked
Blocked shortcut intercepted.
shortcut: string
screenshot.attempted
Likely OS-level screenshot.
trigger: "keyboard" | "visibility" | "blur"
session.checkpoint
A runtime checkpoint ran — re-verifies the live environment against preflight. Emitted every run as an audit record.
ok: boolean · changes: CheckpointChange[]
session.integrity-hold
A checkpoint failed mid-exam and the recovery gate went up — the candidate is blocked until the environment is fixed (device reconnected, permission re-granted, monitor removed). Pairs with session.integrity-cleared.
The environment recovered and the hold lifted — the exam resumes. Correlated to its hold by holdId; the two bracket the interruption window on the timeline.
holdId: string · resolvedMs: number
Together, session.integrity-hold / session.integrity-cleared bracket a paused window (correlated by holdId) — distinct from session.checkpoint, which is a raw per-run audit record. Only the drifts that actually gated the candidate raise a hold. In the Vue wrapper they are emitted automatically; the awaited onIntegrityHold / onIntegrityCleared hooks let you pause and resume your own exam timer around the same window.
The screen-share.* events are live client signals. The screen.* events are emitted post-session by the screen-analysis pass over the stitched screen recording — they catch in-browser window switches that client-side focus.lost never sees.
Emitted by the YOLO object-detection pass that runs alongside face analysis on the stitched webcam recording. Each *-detected event carries the bounding box and confidence; the matching *-cleared fires when the object stops being detected for a debounce window.
Event kind
Meaning
webcam.phone-detected
A cell phone visible in frame.
webcam.phone-cleared
The phone left the frame.
webcam.book-detected
A printed book or notebook visible in frame.
webcam.book-cleared
The book left the frame.
webcam.second-device-detected
A laptop other than the candidate's primary visible in frame.
webcam.second-device-cleared
The second device left the frame.
webcam.second-screen-detected
A TV or monitor other than the candidate's primary visible in frame.
The photo-analysis worker runs the same detectors against each random webcam snapshot after session end, as a backstop to the stitched-video pass. These use distinct -at-snapshot kinds (source "photo-analyzer") so reviewers can tell continuous-video signals from intermittent snapshot sampling; every payload carries photoId so the dashboard can display the original JPEG inline.
Event kind
Meaning
face.lost-at-snapshot
A snapshot showed no face.
face.multiple-at-snapshot
A snapshot showed more than one face.
face.gaze-off-screen-at-snapshot
A snapshot showed gaze directed off-screen.
face.identity-mismatch-at-snapshot
A snapshot's face differs from the verified preflight face.
Emitted by the preflight wizard, which documents the flow, payloads, and materialised outcome in detail. The server folds these into the per-attempt preflight outcome keyed off the same correlationId.
Event kind
Meaning
preflight.started
A preflight wizard attempt began.
preflight.row-checking
A check row (camera / mic / speaker / …) started verifying.
preflight.row-passed
A check row passed.
preflight.row-failed
A check row failed, with its fail code.
preflight.row-retried
The candidate retried a failed check row.
preflight.attestation
The candidate self-attested to an environment requirement.
preflight.device-picked
The candidate selected a device in a row's picker.
preflight.devices-enumerated
Initial media-device list for a row's picker after permission resolved.
preflight.devices-changed
The device list changed mid-flow (plug/unplug), with the new snapshot.
preflight.microphone-verification
Diagnostic: whether Silero VAD verified speech or the bounded RMS fallback did, plus VAD preparation duration and fallback reason.
preflight.face-detected
The live face-detection result during the identity step.
Emitted by <ProctoredTextarea> via client.recordTextAnswer(); see writing answers for payloads and integration. Every payload carries questionId so multiple writing questions in one session stay separable.
Event kind
Meaning
text.started
The candidate began (or resumed) an answer.
text.paste
A paste into the field, with length and pasted ratio.
text.copy
The candidate copied from the field.
text.cut
The candidate cut from the field.
text.keystroke-summary
A sampled window of typing dynamics (counts only — never the keys themselves).
text.checkpoint
A periodic snapshot of the answer so reviewers can replay how it grew.
text.focus-lost
The candidate left the field mid-answer (distinct from window-level focus).
text.focus-regained
The candidate returned to the field.
text.synthetic-input
The answer changed without a matching keystroke, or a writing-assistant footprint was seen.
text.saved
The host's saveAnswer hook resolved.
text.save-failed
The host's saveAnswer hook rejected, with a stable code and reason.
text.submitted
The candidate submitted, with the answer's integrity summary.