The preflight wizard and checklist UIs shown here are Vue components (available today). Their engine — SystemCheck in @a4anthony/proctorkit-sdk — is framework-agnostic and available for custom UIs today; only the React wrapper is 🚧 in progress. See Choose your integration.
The Vue wrapper includes two preflight presentations that check browser compatibility, device access, and configured policy before the assessment begins. The established wizard remains the default; the opt-in checklist reuses the resume-style persistent rows. Both resolve the internal attempt, run the same checks and pass criteria, handle preflight and skip logic, wire face-photo detection, start the SDK from the candidate's Begin test click, and expose the active ProctoringClient through a scoped slot.
Install the Vue package with the SDK peer dependency and render ProctoredAssessment around the assessment UI. The assessment slot should render only after the wrapper has completed the configured preflight and started the session.
This option is presentation-only and intentionally does not live in ProctoringPolicy. The checklist uses the same system engine, VAD-first microphone verification, bounded internal RMS fallback, speaker attestation, deep camera check, screen-share grant, device handoff, telemetry, and final startup path as the wizard. A mounted assessment selects exactly one renderer, so the choice itself does not duplicate permission prompts, media streams, network requests, or preflight events. Treat preflightUi as fixed for the lifetime of a mounted assessment; changing it mid-preflight restarts the presentation.
The checklist remains inside one persistent card for the entire flow. Its order is: system and connection → microphone → speaker → camera and identity → optional candidate intake → screen sharing → final start action in the card footer. Disabled media rows are omitted. When candidate.name is supplied, the toolbar shows a user icon with a localized “Welcome, FirstName” greeting on the left and the global Help and locale controls on the right; it omits the greeting when no name is available. Only the current interactive row expands; completed and upcoming rows remain visible. System and connection is the exception: it stays collapsed with a trailing spinner while its checks run automatically in the background, then advances without a candidate Continue action when every check passes. It expands only when a check fails and shows only the failed checks' recovery guidance, not the successful browser, device, layout, monitor, and connection results. Every expanded failure includes Need help?, which opens the relevant troubleshooting article; when Retry is available, both actions share the same action row. Candidate details uses the same flat expanded-card layout as the media checks: its description, configured fields, inline validation, and right-aligned Continue action render without a second nested panel, while submission failures use the shared error alert. The microphone, speaker, camera, and screen-sharing rows use the same flat layout and shared alert treatment: permission and recoverable error states keep Need help? beside the relevant Allow, Check, Play, Begin, or Try again action, while verified states remove Help and show only Continue. Screen-sharing failures use the shared error alert, and the successful state retains the instruction to hide the browser sharing bar. On the unverified speaker screen, Need help? sits beside Play test tone and opens the slideover directly at speaker troubleshooting. The confirmation checkbox remains visible but disabled before and during playback; only a successfully completed tone enables it. The speaker instruction remains unchanged before, during, and after playback. Help remains available throughout, then is removed after verification. A retryable environmental issue includes Retry; that action reruns only the system-class checks and does not probe microphone or camera in the background. A deterministic incompatibility keeps the guidance and contextual help visible without offering a retry that cannot succeed. The classic wizard uses the same compatibility → media checks → optional candidate intake → screen share → final order.
Mid-assessment refresh recovery reuses that same persistent checklist shell and the shipped SystemStep, MicStep, SpeakerStep, CameraStep, RuntimeScreenShareStep, and FinalStep implementations. The assessment-level candidate greeting, global Help, locale selector, and dark theme remain in place while recovery runs. The refresh probe still determines which requirements need attention, but the candidate resolves them through the same permission, device, microphone-VAD, speaker-tone, camera-preview, and screen-sharing flows used before the assessment. When microphone, speaker-enumeration, or camera permission is lost, resume recovery clears only that saved device selection and requires a fresh selection and verification after access is restored. Dropdown and automatic default selections remain pending and are not saved as runtime device IDs; only the verified Continue action commits the corresponding microphone, speaker, or camera ID. After verification, that action clears the recovery item and advances to the next required check. The final Resume action remains gated until every required recovery step passes.
After microphone, speaker, or camera verification, its collapsed checklist row shows the selected hardware label instead of a generic pass icon.
The wrapper owns the candidate's journey — resolving the attempt, preflight, session start, recovery — and hands you the exam surface through slots. Each renders in a distinct, mutually-exclusive state:
Slot
Renders when
Fallback if you don't provide it
#default
The session is live (or renderOnly is set)
— (nothing renders; this is your exam UI)
#ready
The attempt is resolved and preflight passed, awaiting the candidate's Begin
Built-in ready card
#ended
The assessment is over — see below
Built-in "Assessment submitted" card (suppress with hideEnded)
Use <ProctoringSystemCheck> or <ProctoringChecklist> directly only when you own attempt/session orchestration. They expose the same props, events, verification rules, ordering, and footer-extras slot; only presentation differs. <ProctoredAssessment> supplies most of these props for you.
Custom Vue preflight UIs built with usePreflightCheck() can call retrySystemClass() to reset and rerun only browser, device, layout, monitor, and connection. Use retry() only when the UI intentionally needs a new full preflight run, including configured media checks.
Prop
Default
Purpose
mode
normal
System-check execution mode.
engineOptions
{}
Raw SystemCheckOptions excluding mode.
fullPolicy
—
Complete resolved policy recorded with preflight.started; omit only for a truly standalone wizard.
correlationId
—
Enables preflight telemetry and links it to runtime.
ingestUrl
http://localhost:3001/ingest
Full ingest URL used when correlationId is set.
appId
—
Public pk_ key for scoped preflight requests.
candidate
—
Optional candidate id/name/email/metadata sent with telemetry.
candidateIntake / submitCandidateIntake
—
Optional host-owned intake fields and awaited submit callback.
autoStart
true
Start the wizard automatically.
autoResumeSeconds
5
Countdown used by the resumable flow.
starting
false
Keep the final action disabled/spinning while the host starts runtime.
flushBeforePass
true
Await final telemetry before pass; the wrapper sets false to preserve user activation.
runtimeScreenShareRequired
false
Add the runtime share action/state to the wizard.
runtimeScreenShareState / runtimeScreenShareError
idle / —
Host-controlled share progress and failure copy.
vadAssets
server defaults
Optional VAD and ONNX asset base paths.
messages / preferredLocale
automatic
Typed copy overrides and initial locale. Host overrides have highest priority.
simulateResults
—
Deterministic development/test results; never expose on production candidate links.
allowDevSkip
true
Allows the dev skip only when the URL also carries ?pctDevSkip; set false in hardened builds.
goodReferencePhoto
bundled placeholder
Reference guidance image.
dark
false
Dark visual treatment.
Event
Payload
pass
none
fail
Array<{ kind, code, detail }>
runtime-screen-share-start
none; host must request sharing from this user action
candidate-intake-submitted
CandidateIntakeSubmission
locale-change
selected LocaleCode
The footer-extras slot appends host content to the final step. Branch on typed kind/code values; candidate-facing detail and translated messages are not stable programmatic contracts.
#ended has two entries, and renders identically for both:
A live finish — the session ended during this load. A session-ended event fires.
An already-completed attempt — the candidate reloads (or revisits) a finished assessment. The wrapper resolves the attempt as complete and goes straight to #ended. No session-ended event fires — no session ran this load.
Render your END screen off the slot, not off the session-ended event. The event only covers entry 1. session-ended's own semantics are unchanged — it still means "a session ended during this load".
The slot's live prop discriminates them when you need to: true for a live finish, false for an already-completed attempt. Most hosts can ignore it and render the same screen for both — that's the point.
Only genuinely-unstartable attempts still hit the blocked surface (hideBlocked to suppress it): a session mid-teardown, one live on another device, or a failed resolve.
A preset is a complete proctoring policy — every preflight check and runtime observer set for you. Pick one with the preset prop; it is the single most important configuration choice. There are three, increasing in strictness:
Preset
Use it for
In one line
basic
Low-stakes / unproctored-but-monitored
Browser check + activity signals only. No camera, no recording.
standard(default)
Most proctored exams
Camera/mic/speaker preflight + face photo, fullscreen, clipboard/keyboard/focus monitoring, webcam snapshots, face presence. No recording, no screen share.
strict
High-stakes / identity-critical
Continuous webcam video and screen-share recording, plus the standard monitoring signals.
✓ = on, – = off. Preflight rows are checks the candidate clears before the test; runtime rows are what's monitored/recorded during it.
Capability
basic
standard
strict
Preflight — browser & system checks
✓
✓
✓
Preflight — camera (+ face photo)
–
✓
✓
Preflight — microphone
–
✓
✓
Preflight — speaker
–
✓
✓
Preflight — fullscreen support
–
✓
✓
Preflight — screen-share grant
–
–
✓
Runtime — heartbeat / idle
✓
✓
✓
Runtime — focus & tab-visibility
✓
✓
✓
Runtime — fullscreen enforcement
–
✓
✓
Runtime — clipboard monitoring
–
✓
✓
Runtime — keyboard monitoring
–
✓
✓
Runtime — webcam snapshots
–
✓
–
Runtime — webcam video recording
–
–
✓
Runtime — screen-share recording
–
–
✓
Runtime — face presence (lost / multiple)
–
✓
✓
Runtime — gaze tracking
–
✓
✓
Runtime — identity mismatch
–
✓
✓
Screen share is a single switch: the preflight screen-share grant is shown only when the session records the screen (strict). You never wire it twice. It's a live permission, not an acknowledgement — the step runs a real getDisplayMedia prompt and enforces the shared surface (entire screen, not a window or tab). It sits after candidate intake, before the final step; if the share drops while the candidate is on the final step, the wizard returns to the grant step.
Webcam capture is also a single choice: none, snapshots, or recording. Camera and face-photo preflight follow that choice automatically; snapshots and recording cannot run together.
preset sets the baseline; policy-overrides adjusts individual values on top (a deep partial — you only specify what differs). Use this to start from a preset and flip one thing, rather than spelling out a whole policy.
vue
<ProctoredAssessment
preset="standard":policy-overrides="{
proctoring: { clipboard: false }, // standard, but allow paste
preflight: { microphone: false }, // …and skip the mic check
}".../>
Precedence is policyOverrides › preset › the standard default — later wins, merged deeply per field. (A preset key inside policyOverrides also selects the base, so you can pick everything through overrides alone.)
preflight.enabled controls whether the wrapper runs the preflight wizard:
Value
Effect
true(default for every preset)
The candidate completes the configured preflight checks before the session starts.
false
Skip the wizard. Runtime monitoring still follows the proctoring branch.
Set it via policy-overrides: :policy-overrides="{ preflight: { enabled: false } }".
The server also enforces the preflight pass at ingest. With the default strict gate, session.* events for a session whose latest wizard attempt has not passed are rejected with 403 (preflight.* batches are never gated). The gate is a deployment setting — PREFLIGHT_GATE=strict|warn|off — so if your integration intentionally skips the wizard, the deployment must run warn or off, or every runtime event will bounce.
Identity mismatch requires the face reference captured during preflight. A policy with preflight.enabled: false must use webcam.mode: "none" or explicitly set webcam.faceAnalysis.identityMismatch: false; otherwise policy validation fails.
Legacy verificationMode inputs remain accepted during migration: preflight_required maps to preflight.enabled: true, while runtime_only and disabled map to false. The legacy field is removed from the resolved and stored policy.
developmentMode (under preflight) relaxes checks for local development only — never ship it on, and never let candidate-link query parameters disable a required check.
Thresholds should be conservative for V1 demos. Use preflight to catch unsupported browsers, missing devices, slow network, camera/microphone denial, speaker failures, a declined or wrong-surface screen-share grant, fullscreen support, and face-photo verification where supported.
The on/off toggles decide which rows run. thresholds decides what counts as a pass. Every field is optional with a documented default; the table below is the full surface.
ts
engineOptions:{
thresholds:{
minBandwidthMbps:5,// raise from default 2 → 5
minUploadBandwidthMbps:3,// production-path upload floor
micMinSpeechMs:3000,// 3s of speech (high-stakes)
maxFacesAllowed:1,// exactly the candidate
allowExternalMonitor:false,// block multi-monitor
allowMobile:false,// block phones (incl. Android)},}
Field
Type
Default
Meaning
minBandwidthMbps
number
2
Lower bound on the speed test. Below trips slow-connection.
minUploadBandwidthMbps
number
2
Lower bound on the application-path upload measurement. Evaluated before download because continuous recordings depend on upload capacity.
micMinSpeechMs
number
1500
Continuous speech required before mic verifies. Higher = stricter.
maxFacesAllowed
number
1
Faces allowed in frame for camera pass. Above this fails as multiple-faces.
allowExternalMonitor
boolean
false
Permit candidates with a second display attached.
allowMobile
boolean
false*
Permit candidates on Android phones / iPads (iOS gated by enableSafari).
SystemCheckOptions.requiredCapabilities.screenShare requires browser getDisplayMedia support during the initial device check. <ProctoredAssessment> sets it automatically from proctoring.screenShare.enabled; raw SystemCheck consumers can set it directly. Because this is a runtime prerequisite, the device gate remains present even if system.device is false. It is capability-based rather than user-agent-based, so mobile browsers are blocked today while a Windows or ChromeOS tablet with a capable desktop browser can pass.
* false is the raw SDK-engine default only. Every <ProctoredAssessment> preset (basic, standard, strict) sets allowMobile: true — so under any preset, mobile is allowed unless you override it:
The external-monitor check is also re-run on a mid-session refresh (the resume re-check), mirroring this policy — so a second display connected mid-exam is caught on the way back in, not just at first entry. It respects allowExternalMonitor and developmentMode the same way.
The microphone step acquires the candidate's selected input only after permission and a Test microphone click. Silero VAD is the primary verifier because it distinguishes speech from steady noise. The model/runtime preparation begins in the background when the microphone step mounts, without opening the microphone, so cached or fast-network candidates usually avoid a cold-start wait.
When the microphone step mounts, it first reads the browser's stored microphone permission without opening the device. The permission CTA stays hidden while that local check resolves. If access is already granted, the step opens directly on device selection and Test microphone; otherwise it shows Allow microphone. Browsers that cannot report microphone permission fall back to the normal permission CTA. This prevents a remembered grant from briefly flashing the wrong permission screen.
After the candidate clicks Test microphone, the UI waits at most 10 seconds for VAD preparation and live initialisation. If that deadline expires or VAD initialisation fails, an internal RMS reliability fallback takes over for that attempt. The fallback requires sustained input and begins its silence window only after it is actually processing the live stream; it never shows a “no sound” failure merely because the VAD model is still downloading.
This behaviour is internal and always enabled. verificationMode, fallbackReason, and vadPreparationMs are diagnostic fields on preflight.microphone-verification, not props or policy controls. A typical timeout diagnostic is:
Customers may override only the asset locations through vadAssets.baseAssetPath and vadAssets.onnxWASMBasePath. By default the API serves the VAD assets under /sdk-assets/vad/ and ONNX Runtime under /sdk-assets/ort/. <ProctoredAssessment> forwards the same resolved paths to initial preflight, mid-assessment resume checks, and microphone verification in Device Settings. Host them on the same reliable origin as the API or pre-warm them through normal HTTP caching; do not gate the assessment indefinitely on model delivery.
Face-photo verification runs during preflight when configured. That passing reference is used by post-session snapshot/recording analysis for identity mismatch. Face presence, gaze, identity, and object signals are automated review evidence—not proof of misconduct—and depend on the selected webcam evidence mode and analyser support.
The camera step's deep check always runs through a detectFace callback. The engine enforces face count: zero faces fails as no-face (a covered lens or an off-camera candidate both yield zero faces); more than maxFacesAllowed fails as multiple-faces. The engine fails closed — a missing detector fails the check as deep-check-failed rather than silently passing. You rarely wire the callback yourself: when the camera check is enabled and you don't supply detectFace, the wizard auto-wires the server-side detector described below.
Proctor runs the face-detection service for you, behind two endpoints — the frame is never sent to a third party either way:
POST /sessions/:sessionId/preflight/camera-check/:attempt — the production path, and the default whenever a correlationId is set (always the case inside <ProctoredAssessment>). The backend stores the exact frame, runs detection, links the photo to the preflight attempt, and returns the verdict — so the dashboard's photo strip shows the very frames the check judged. Wire it manually with createNestPreflightCameraCheck.
POST /preflight/detect-face — stateless detect-only; returns the face count, persists nothing. The fallback when no correlationId is set (a standalone wizard). Wire it manually with createNestDetectFace.
ts
import{ProctoringSystemCheck,
createNestPreflightCameraCheck,
createNestDetectFace,}from"@a4anthony/proctorkit-vue";// Production path — session-scoped, persists the frame:const detectFace =createNestPreflightCameraCheck({
baseUrl:"https://api.proctor.app",
sessionId,// internal `sess_…` id from /sessions/resolve-attempt
appId:"pk_live_xxx",
maxFacesAllowed:1,
timeoutMs:8_000,// default// headers: {}, // extra headers on every request});// Stateless path — detect-only, nothing stored:const detectFaceStateless =createNestDetectFace({
baseUrl:"https://api.proctor.app",
timeoutMs:5000,// default// ?embeddings=1 forwards the 512-dim ArcFace vector// for identity matching. Default off.
includeEmbeddings:false,// headers: {}, // extra headers on every request});h(ProctoringSystemCheck,{
engineOptions:{
deepCamera:{ detectFace },},});
Embeddings are off by default because they're biometric data the candidate-facing wizard doesn't need. Flip includeEmbeddings: true when you wire identity matching against an enrolled photo.
You can supply any function matching the SDK contract (jpegDataUrl) => Promise<{ faceCount }>. For maximum privacy, run MediaPipe Tasks Vision in the browser — no upload, no server, ~2 MB of WASM cached after first visit.
Client-side detection trades privacy for control. You can't update the model without a new SDK version, and you lose the central audit log of every detection verdict that the server-side path gives you.
There is no "no detector" mode. When the camera check is enabled and you leave detectFace out, the wizard auto-wires the server-side detector from your ingestUrl — createNestPreflightCameraCheck when a correlationId is set, createNestDetectFace otherwise. And if no detector is wired or reachable, the engine fails closed: the deep check fails as deep-check-failed ("Face detector not configured") — it never passes on a bright frame alone.
All platform-owned copy used by preflight and resume recovery is published through Content Studio schema v4 and represented by the typed PreflightMessages bundle. This includes permission prompts and browser recovery guides, system-detail templates, card status labels, candidate-intake defaults and validation, headshot and photo failures, screen-share and runtime-startup failures, and device-setting controls. The bundled English catalog is only the network-safe fallback. An explicit messages prop can still override a subset for an embedded integration:
Refresh-recovery chrome and reason-specific media diagnoses live under
messages.resume, including resume.recovery.microphone,
resume.recovery.speaker, and resume.recovery.camera. The recovery wizard
otherwise reuses the normal step copy under wizard, runtime, and
checklist, so the same overrides and active locale apply before and during an
assessment. Failure-specific system headings may be overridden through
failCodeHeaders; the generic failHeaders remain the fallback by check kind.
Detected and customer-authored runtime data is not Studio copy. Candidate names, device names, filenames, browser and operating-system values, screen dimensions, measured speeds, and candidate-intake titles, section headings, field labels, options, help text, and guidelines come from the current candidate, browser, or host field configuration. Components interpolate those values into Studio-managed templates. Candidate-facing errors never expose raw browser exceptions, media-adapter diagnostics, upload errors, or host callback messages.
All package styles are scoped to the .pct-root root so they never collide with the host's own Tailwind config or plain CSS. The bundled stylesheet does not require Tailwind in the host app and does not emit global html, body, :root, :host, or --font-sans rules. Override tokens via CSS variables at that root — every component (the wizard, <ProctoredAssessment>, and the media controls) renders under .pct-root, so one block re-themes all of them. ProctoredAssessment scopes only package-owned UI; your assessment slots render outside that root and keep your app's own font and layout.
css
.pct-root{--color-pct-brand: oklch(0.60.2250);--color-pct-success: oklch(0.70.15145);/* Body font. */--font-pct:"Your Brand Font", system-ui, sans-serif;/* Monospace bits (timestamps, counters) — a SEPARATE, namespaced token. */--font-pct-mono:"Your Mono", ui-monospace, monospace;/* Defaults are Inter-specific glyph alternates; disable for other fonts. */--font-pct-feature-settings: normal;}
Fonts.--font-pct sets the body font and --font-pct-mono the monospace one (timestamps/counters). Both are pct--namespaced so they never touch your app's own --font-sans / --font-mono. They're independent — overriding only --font-pct leaves the mono bits on the default. The package bundles no font face; load yours (e.g. a Google Fonts <link>) or it falls back to the OS default. These tokens live in @layer theme, so a normal rule in your app overrides them without !important. If you give --font-pct-mono a value built from var(--font-sans, …), include a fallback inside the var() so an undefined token can't invalidate it.
For dark mode, pass dark as a prop and the wizard inverts ink + surface tokens.
On pass, the final step counts down before emitting pass — in boot and normal modes alike. Only recheck mode (the mid-session resume re-check) skips the countdown and always waits for an explicit click. Set autoResumeSeconds: 0 to require the click everywhere.
The camera, microphone, and speaker steps keep their dropdowns in sync with the hardware. If a candidate plugs in a Bluetooth headset or a webcam after the step has loaded, it appears in the picker automatically — each step re-scans on the browser's devicechange event (debounced, and the current selection is preserved). A Refresh button below each dropdown lets the candidate force a re-scan too, for the rare browser that doesn't fire the event. No configuration; it's on by default.
During a running Vue assessment, the host can open the wrapper-owned device settings dialog with client.openSettings(). It composes these same MicStep, SpeakerStep, and CameraStep components—not parallel versions—inside its policy-filtered checklist. A different selection reveals the matching verification flow, and the verified replacement is staged until the candidate clicks Save and close.
Candidate intake can collect customer-defined candidate fields before test start. Validate required fields before starting proctoring and do not send sensitive identity documents to third-party telemetry by default.
An optional schema-driven step collects identity details before the test starts — date of birth, ID document uploads, a live headshot, and any custom fields. Candidate details always follows Camera and identity when that check is present. A required headshot automatically adds the camera preflight check even when proctoring.webcam.mode is none; this grants and verifies camera access but does not enable runtime webcam snapshots, recording, or identity-mismatch analysis. The headshot field reopens the selected camera under the already-granted permission and still owns its capture lifecycle. Optional headshots do not force the camera check. Enable intake with two props on ProctoredAssessment, ProctoringSystemCheck, or ProctoringChecklist: candidateIntake (the field config) and submitCandidateIntake (your save handler). Omit the config and the row disappears entirely.
candidateIntake is a plain, serialisable object, so you can drive it from your backend's per-test settings. Field types: text (with uk_postcode or a custom regex pattern), date, select, checkbox, file (accept list, per-file and total size caps), and headshot (a live camera capture, verified to contain exactly one face). Mark any field pii: true so your backend knows what to treat as sensitive.
Compulsory fields display a red asterisk beside their label and expose the required state to assistive technology. Text, date, select, and file-upload validation errors are anchored immediately below their control in a reserved error lane. Invalid text, date, and select controls receive a red border and soft red background; an invalid file field applies the same treatment to the complete upload dropzone. Errors remain visually associated with their field without overlapping the next configured field or shifting the form.
ts
importtype{CandidateIntakeConfig}from"@a4anthony/proctorkit-vue";const candidateIntake:CandidateIntakeConfig={
title:"Confirm your details",
description:"Required by this assessment provider before the test starts.",
fields:[{ key:"dob",type:"date", label:"Date of birth", required:true, pii:true},{
key:"postcode",type:"text",
label:"Postcode",
required:true,
pii:true,
validation:{ pattern:"uk_postcode"},},{
key:"documents",type:"file",
label:"Upload ID",
required:true,
pii:true,
accept:["application/pdf","image/jpeg","image/png"],
maxFiles:2,
maxFileSizeBytes:10_000_000,},{ key:"headshot",type:"headshot", label:"Take a headshot", required:true, pii:true},],};
Keep fields as the single source of field definitions and add sections when the form would otherwise be too long. Each section references existing field keys in display order. Every field must be assigned exactly once, section IDs must be unique, and unknown field keys are rejected. Omit sections to retain the existing single-page form.
ts
const candidateIntake:CandidateIntakeConfig={
title:"Candidate details",
description:"Confirm the information required for this assessment.",
fields:[{ key:"dob",type:"date", label:"Date of birth", required:true, pii:true},{
key:"gender",type:"select",
label:"Gender",
required:true,
options:["Female","Male","Non-binary","Prefer not to say"],
pii:true,},{ key:"postcode",type:"text", label:"Postcode", required:true, pii:true},{
key:"documents",type:"file",
label:"Upload documents",
required:true,
maxFiles:3,
pii:true,},{ key:"headshot",type:"headshot", label:"Take a headshot", required:true, pii:true},],
sections:[{
id:"personal-details",
title:"Personal details",
description:"Enter the details associated with your assessment.",
fieldKeys:["dob","gender","postcode"],},{
id:"documents",
title:"Supporting documents",
fieldKeys:["documents"],},{
id:"headshot",
title:"Identity photo",
fieldKeys:["headshot"],},],
backLabel:"Back",
nextLabel:"Next",
continueLabel:"Confirm and continue",};
Selecting Next validates only the visible section. Back preserves entered values and selected files. The host's submitCandidateIntake callback still runs exactly once, after the final section, with the same CandidateIntakeSubmission shape used by ungrouped forms. The component keeps a bounded internal scroll area as a fallback if one configured section is itself unusually long. Select menus render above that boundary, so their options remain visible instead of being clipped by the scroll area.
Use the exported validateCandidateIntakeSections(config) helper in an assessment-builder or CI validation step. The live component also validates the map and fails closed with a candidate-safe configuration message rather than silently omitting or duplicating fields.
submitCandidateIntake is an async function you supply. The wizard calls it with the validated submission, awaits it, and reads success or failure from the result. You signal failure in whichever way fits:
Return true / false — the simplest form. false fails with a generic message; true (or returning nothing) succeeds.
Return { ok: false, message } — fail and retain the optional message for source compatibility and host diagnostics. Candidate UI uses the Content Studio save-failure message.
throw an Error — also fails. The exception remains available to host diagnostics, while candidate UI uses the Content Studio save-failure message.
On any failure the wizard stays on the step, keeps the candidate's entered values and uploaded files, shows the published Studio message, and lets them retry. On success it advances and fires candidate-intake-submitted. The data goes to your backend — the proctoring platform never receives or stores intake PII; you own the endpoint, storage, retention, and encryption.
The simplest version — return the result of your save:
ts
asyncfunctionsubmitCandidateIntake(submission){const res =awaitsaveToYourBackend(submission);// your store action / fetch wrapperreturn res.ok;// true → success, false → failure (generic message)}
The full version, with host-side diagnostics and the one gotcha to watch — fetch() does not throw on 4xx / 5xx, it resolves with res.ok === false, so a failed request would otherwise look like success:
ts
importtype{CandidateIntakeSubmission}from"@a4anthony/proctorkit-vue";asyncfunctionsubmitCandidateIntake(submission:CandidateIntakeSubmission){// submission.values is keyed by your field keys. File / headshot values// are arrays of { file: File, name, size, type, lastModified }, so you// can stream the real File objects into FormData.const form =newFormData();for(const[key, value] of Object.entries(submission.values)){if(Array.isArray(value)) value.forEach((f)=> form.append(`${key}[]`, f.file, f.name));else form.append(key,String(value));}
form.append("submittedAt", submission.submittedAt);let res:Response;try{
res =awaitfetch("/your-backend/intake",{ method:"POST", body: form });}catch{// Network / timeout — throw for host diagnostics. Candidate copy comes from Content Studio.thrownewError("candidate intake request failed");}if(!res.ok){// Server rejected it — retain a diagnostic message for host logging.const body =await res.json().catch(()=>null);return{
ok:false,
message: body?.message ??"candidate intake rejected",};}return{ ok:true};}
You do not manage loading or error UI: the wizard shows a "Saving…" state, disables the button while awaiting, re-enables on failure, and preserves the form. You only report the outcome.
candidate-intake-submitted fires only on success (after your callback resolves), with the resolved attempt — the place to mark the attempt intake-complete on your backend.
Failures should be actionable for candidates and useful for operators. Explain which permission or compatibility issue blocked the candidate and surface enough state in the dashboard to support the user without exposing private media unnecessarily.
Every failure code maps to a tailored recovery panel. Candidates fix the issue inline and retry without leaving the wizard. Codes appear in the engine's CheckRow.state.code and in the wizard's fail event, which carries the full failure list as Array<{ kind, code, detail }>.
FailCode
Row
Recovery
unsupported-browser
browser
Switch to a supported browser.
outdated-browser
browser
Update to a recent version.
ios-device
device
Use desktop (gated separately by enableSafari).
incompatible-device
device
Mobile not allowed by policy. Use a desktop.
low-memory
device
Free RAM or switch machine.
bad-layout
layout
Rotate to portrait on mobile; ignore on desktop.
external-monitor
monitor
Disconnect external displays (override with allowExternalMonitor).
offline
connection
Reconnect to a network.
slow-connection
connection
Switch networks or move closer to the router.
speed-test-failed
connection
The connection measurement could not complete; candidate remains blocked and can retry.
permission-denied
microphone / camera
Shared error alert followed by detected-browser Block → Allow steps; Need help? opens permission recovery, with Try again beside it.
permission-required
microphone / camera
Candidate has not yet granted. Wizard prompts.
device-disconnected
microphone / camera
Reconnect device or pick another from the dropdown.
no-device-found
microphone / camera
Connect a device.
no-face
camera
Uncover lens, sit in front of the camera, face it.
Detector errored. Candidate retries; admin watches the rate.
screen-share-not-supported
device / screen-share
A required screen-sharing capability is missing. Switch to a supported desktop browser.
screen-share-declined
screen-share
Candidate cancelled the share picker. Reopen it and choose a surface.
screen-share-wrong-surface
screen-share
Candidate shared a tab/window instead of the whole screen. Reshare and pick the entire screen.
A speaker-only flow still needs microphone permission because browsers use it to reveal audio outputs. If that permission is denied, the speaker card shows the same detected-browser Microphone Block → Allow guide and routes Need help? to permission recovery.
The first passing preflight attempt enrols the candidate: its device fingerprint and face embedding become the session's canonical identity. Every later signal is compared against that enrolment, and drift flips requiresReverification to true on the session:
A different device on a later wizard attempt — the new attempt's fingerprint scores past the difference threshold against the canonical one.
A mid-session device swap — runtime session.fingerprint events reveal the session continuing from different hardware.
A browser-family swap on identical hardware — Chrome → Safari on the same machine always trips, regardless of score.
The flag clears itself the same way it set: a fresh passing wizard attempt — whose deep camera check verifies the live face against the canonical embedding — resets requiresReverification to false. A failed or abandoned re-attempt leaves it standing.