Overview
The LMS creates a session (POST /v1/exam/create-session), sends the candidate to the returned launchUrl, and polls GET /v1/exam/status/{id} until terminal is true — then acts on the verdict: ready start the exam, blocked don't, expired setup failed. Supply a callbackUrl to also receive signed webhooks for live, mid-exam violations. Poll for the gate because the LMS is actively waiting and a dropped response just retries; webhook for live events because the LMS isn't polling during the exam itself.
Quick start
Everything below is runnable straight out of the downloaded package.
WATCHR_BASE=https://watchrproject.com \ WATCHR_API_KEY=your_key \ node examples/gate-client.js
WATCHR_WEBHOOK_SECRET=your_shared_secret \ node examples/webhook-receiver.js
cd mock-lms WATCHR_TARGET=droplet WATCHR_API_KEY=your_key WATCHR_WEBHOOK_SECRET=your_secret npm start # open http://127.0.0.1:4000
The mock LMS dashboard creates a session, opens the scanner, polls the gate live, shows every webhook with a signature-valid / SIGNATURE INVALID badge, and hands off to a login-gated mock exam (candidate@example.com / exam123) once the gate is clean.
Authentication
Three different callers, three different credentials.
| Caller | Header | Notes |
|---|---|---|
| Your LMS → Watchr | X-Api-Key | Required on create-session and status. Set server-side via WATCHR_API_KEY; a missing or invalid key is rejected with 401. |
| Watchr → your LMS | X-Watchr-Signature | sha256=<hmac> over the raw webhook body, keyed with the webhookSecret you supplied at session creation. Verify before trusting the payload. |
| Candidate's browser → Watchr | per-session token | A sessionToken embedded in the launch page. Your LMS never needs to see or handle it. |
Session lifecycle
blocked is resolvable — a clean re-scan can move the session back to ready. expired fires automatically once gateTimeoutSeconds elapses on any non-terminal session, so the LMS never polls forever.
gateTimeoutSeconds passes
The OpenAPI schema also lists a terminated status value not yet covered by the integration guide above — check with the Watchr team before building logic against it.
Detection categories
Detection is layered by source. The agent is authoritative for anything requiring system visibility; the browser contributes only high-confidence signals. Violations reported through the API carry a free-form module and name — "screencast" / "Screen Recording" is one real example from the reference docs.
| Category | Source | Examples |
|---|---|---|
| Screen recording | agent | CamStudio, ShareX, Snagit, OBS |
| Remote desktop | agent | TeamViewer, AnyDesk, RDP, VNC |
| Prohibited apps | agent | Office, notes, messaging, AI tools |
| Virtual machine | agent + browser | VMware, VirtualBox, Parallels |
| Displays / splitters | agent + browser | Second monitor, HDMI splitter (EDID) |
| Screen capture | browser | getDisplayMedia, virtual camera |
| App / tab switching | browser | Focus loss, fullscreen exit |
Create session
| Field | Type | Notes |
|---|---|---|
examId | string | Optional. Max 50 chars. Defaults to "default". |
candidateName | string | Optional. Max 100 chars. |
candidateEmail | string | Optional. Max 150 chars. |
callbackUrl | string (uri) | Optional. Watchr POSTs webhooks here. |
webhookSecret | string | Optional. Max 128 chars. Signs webhook callbacks. |
redirectUrl | string (uri) | Optional. Where to send the candidate after a passing scan. |
gateTimeoutSeconds | integer | Optional. 30–900, default 300. |
expiresInMinutes | integer | Optional. Overall session lifetime, default 120. |
scanConfig | object | Optional. Per-session module configuration. |
{
"sessionId": "EXAM-A1B2C3D4E5F6",
"sessionToken": "…",
"launchUrl": "https://watchrproject.com/v1/exam/launch/EXAM-A1B2C3D4E5F6",
"statusUrl": "https://watchrproject.com/v1/exam/status/EXAM-A1B2C3D4E5F6",
"watchrUrl": "https://watchrproject.com",
"status": "pending",
"gateTimeoutSeconds": 300
}sessionToken is for the browser launch page — your LMS can ignore it.
Errors: 401 bad or missing key · 400 invalid JSON.
Launch page
Serve it via redirect or embed. Returns HTML — the Watchr client with the session token baked in. No API key required; possession of the launchUrl is the capability.
Get status
terminal.
{
"sessionId": "EXAM-A1B2C3D4E5F6",
"status": "pending",
"terminal": false,
"scanResult": null,
"agentConnected": true,
"candidateName": "Jane Doe",
"createdAt": 1730000000000,
"completedAt": null,
"violations": []
}| status | terminal | LMS action |
|---|---|---|
pending | no | keep polling |
scanning | no | keep polling |
ready | yes | start the exam |
blocked | yes | show the violations; do not start |
expired | yes | scan didn't finish in time — show "setup failed, retry" |
Errors: 401 bad or missing key · 404 unknown session.
Update session · internal
Authenticated with the sessionToken, never the API key. A wrong token returns 403. On a ready / blocked / active transition Watchr fires a status_change webhook; when new violations appear it fires a violation webhook.
{ "sessionId", "sessionToken", "status"?, "scanResult"?, "agentConnected"?, "violations"?, "score"? }Webhook callback
Sent if callbackUrl was supplied at session creation. If a webhookSecret was also set, the request carries X-Watchr-Signature. This is a fast-path notification, not the source of truth — for the gate decision, the poll result is authoritative; treat the webhook as an accelerator only.
{
"event": "status_change",
"sessionId": "EXAM-A1B2C3D4E5F6",
"status": "blocked",
"scanResult": "...",
"agentConnected": true,
"candidateName": "Jane Doe",
"violations": [
{ "module": "screencast", "name": "Screen Recording", "evidence": "...", "verdict": "block" }
],
"timestamp": 1730000000000
}computed = "sha256=" + HMAC_SHA256(webhookSecret, rawRequestBody) // hex reject unless constant_time_equals(computed, headers["x-watchr-signature"])
Raw OpenAPI JSON
The same reference above, as an OpenAPI 3.0 document — converted from the docs/openapi.yaml in the download. Drop it into Postman, Swagger UI, or a client generator.