Use webhooks for live evidence and completion summaries.
| Event | When it sends | Payload summary |
|---|---|---|
violation.detected |
After a violation is saved | One violation and the Endpoint session UUID. |
recording.uploaded |
After a recording uploads to your S3 bucket | One partner_s3 recording. |
exam.completed |
After the session reaches completed and no recording upload is pending |
Session summary, all violations, deliverable recordings, and upload errors if any. |
To receive events, configure a webhook URL and webhook secret on your tenant. Event filtering is tenant-level; an empty event list means all events are enabled.
POST <your webhook URL>
Content-Type: application/json
X-Webhook-Signature: <64 hex chars>
X-Webhook-Timestamp: <unix seconds>
User-Agent: EndpointProctoring/1.0
Any 2xx response is success. Current delivery does not guarantee redelivery for all failure types. Return 2xx quickly, store the signed payload or event_id, and process asynchronously.
Every payload has a top-level event_id. Use it as your deduplication key.
Endpoint signs the exact JSON body it sends with:
HMAC-SHA256(webhook_secret, json_body)
The header value is lower-case hex with no sha256= or v1= prefix. Verify against the raw request body bytes before parsing JSON. The timestamp header is not part of the signed value; use it only for your own replay window.
Ruby example:
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, request.raw_post)
valid = ActiveSupport::SecurityUtils.secure_compare(expected, request.headers["X-Webhook-Signature"].to_s)
Node example:
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const valid = Buffer.byteLength(expected) === Buffer.byteLength(signature || '') &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature || ''));
violation.detected{
"event": "violation.detected",
"event_id": "5c3e1f8a-2b6d-57e2-9f1a-6d0c2e7b4a91",
"timestamp": "2026-09-10T14:41:03Z",
"session_id": "9d2b6c31-1f45-4a80-b7de-5c1e0a3f9b22",
"violation": {
"id": 4412,
"type": "tab_switch",
"severity": "medium",
"description": "Switched away from the exam tab",
"occurred_at": "2026-09-10T14:41:03Z",
"auto_detected": true
}
}
recording.uploaded{
"event": "recording.uploaded",
"event_id": "9b1deb4d-3b7d-5bad-9bdd-2b0d7b3dcb6d",
"timestamp": "2026-09-10T15:30:44Z",
"session_id": "9d2b6c31-1f45-4a80-b7de-5c1e0a3f9b22",
"recording": {
"id": 771,
"type": "camera",
"url": "s3://your-bucket/proctoring/9d2b6c31/camera.webm",
"storage_provider": "partner_s3",
"duration_seconds": 5189,
"file_size_bytes": 104857600,
"uploaded_at": "2026-09-10T15:30:44Z"
}
}
exam.completed{
"event": "exam.completed",
"event_id": "0a128f02-a18b-5558-9df7-058104ed5272",
"timestamp": "2026-09-10T15:31:12Z",
"session": {
"id": "9d2b6c31-1f45-4a80-b7de-5c1e0a3f9b22",
"exam_id": "exam-204",
"reservation_uuid": "3f6a1d92-7c40-4b1e-9a55-0e2f8b4c1d77",
"candidate_email": "ada@example.com",
"candidate_name": "Ada Lovelace",
"exam_name": "Network Security Final",
"course_name": "Network Security",
"started_at": "2026-09-10T14:02:11Z",
"ended_at": "2026-09-10T15:28:40Z",
"duration_minutes": 86,
"compliance_score": 90
},
"violations": [
{
"id": 4412,
"type": "tab_switch",
"severity": "medium",
"description": "Switched away from the exam tab",
"occurred_at": "2026-09-10T14:41:03Z"
}
],
"recordings": [
{
"type": "camera",
"url": "s3://your-bucket/proctoring/9d2b6c31/camera.webm",
"storage_provider": "partner_s3",
"duration_seconds": 5189,
"file_size_bytes": 104857600
}
],
"recording_errors": [
{
"type": "screen",
"error": "AccessDenied",
"failed_at": "2026-09-10T15:30:02Z"
}
]
}
duration_minutes is elapsed exam time, not scheduled length. recording_errors is omitted when there are no upload errors.
exam.completed includes only recordings with storage_provider partner_s3 or local_only.
| Provider | Meaning |
|---|---|
partner_s3 |
Uploaded to your bucket. The URL is an s3:// URI; fetch it with your AWS credentials. |
local_only |
Endpoint is holding the file because no partner S3 bucket was configured. The URL is a signed HTTPS URL valid for 7 days. |
failed |
Not listed in recordings; represented in recording_errors when an upload error was recorded. |
skipped |
Not listed in recordings. Screen upload is skipped unless explicitly enabled. |
Current attempt recordings use camera and screen; room-scan video may appear as room_scan_video. Do not reject a payload solely because a future recording type is unfamiliar. Check-in still images are not delivered by webhook in the current flow.
Severity is always low, medium, high, or critical. Treat type as an open catalogue: handle unknown future types generically by severity.
| Type | Typical severity |
|---|---|
tab_switch, window_blur, alt_tab_attempt |
medium |
copy_attempt, cut_attempt, paste_attempt, excessive_selection |
low to high |
right_click, drag_attempt, form_submission_blocked |
low |
print_attempt, print_shortcut, screenshot_attempt |
medium to high |
screen_capture_attempt, screen_share_stopped, devtools_detected |
critical |
devtools_shortcut, console_accessed, camera_access_denied, screen_share_denied |
high |
screen_share_muted |
medium |
navigation_attempt, navigation_blocked, navigation_violation |
low to high |
multiple_displays_detected |
medium |
iframe_injection, suspicious_dom_injection, spoofed_end_signal |
medium to high |
no_person_visible, multiple_people |
critical |
phone_visible, ai_detected_violation |
high |
looking_away, suspicious_object |
medium |
improper_lighting, background_activity |
low |
test_violation is used during staging and is deleted before the real exam starts.
For LTI Proctoring Services sessions, Endpoint sends exam.completed after it receives LtiEndAssessment, the session reaches completed, and no recording upload is pending.
Last verified: 2026-09-03 against main.