Session Integrity Verdicts
Store one integrity verdict per session, then read that row or a test-level roll-up computed over named sessions.
Note: This guide covers the integration flow. See the API Reference for complete endpoint documentation, all parameters, and response schemas.
What is live
- Clock values inside
whyandevidenceare bareHH:MM. They carry no date and no zone. The date comes fromsessionStartedAtandsessionEndedAt. The zone name comes fromtimeZone. Those clock values are UTC today. In the sample below,09:05is 09:05 UTC. On 2026-03-10 that is 04:05 inAmerica/Chicago, not 09:05 local. - No automatic producer is enabled. The vault channel that would carry a structured verdict is not in place. A session with no row returns
verdict: null. That is notGREEN. - StudyFilm is the product that shows a stored verdict. The TimeBack App does not.
How a verdict reaches you
When a verdict exists, an agent judged the session at session end. The vault published the saved report on its channel. The platform stored one row. StudyFilm reads that row.
The TimeBack App does not show it. Any caller with events.readonly can read the same row.
The platform keeps one row per session. A later write for the same session replaces that row.
Prerequisites
-
Register your App and obtain OAuth credentials. See Level 0: Register Your App and Authentication.
-
Request the Caliper event scopes for your M2M client:
- Write:
https://purl.imsglobal.org/spec/caliper/v1p2/scope/events.write - Read:
https://purl.imsglobal.org/spec/caliper/v1p2/scope/events.readonly
- Write:
-
Own the session's organization. A caller without organization access receives
403.
Verdicts
| Value | Meaning |
|---|---|
RED |
Evidence the student obtained, or tried to obtain, answers through unauthorized help. |
ORANGE |
Behaviour an invigilator would note but could not act on. |
GREEN |
Clean. |
GREY |
Nothing to judge. |
greyReason is present if and only if verdict is GREY.
| Value | Meaning |
|---|---|
Test Review |
Every question on screen was already graded, and no attempt is attached. |
No Attempt |
Never started, or left before any question was answered. |
Tags
RED takes one or more of:
Unauthorized help from a PersonUnauthorized help from an ApplicationUnauthorized help from a Device
ORANGE takes one or more of:
Suspected help from a PersonSuspected help from an ApplicationSuspected help from a DeviceSuspicious behaviour
GREEN and GREY take an empty array. A tag from the other family is 400.
Fields
| Field | Meaning |
|---|---|
why |
One sentence, 1 to 500 characters. |
evidence |
The pointer the producer wrote, 1 to 2000 characters. The platform stores it as written and does not parse it. |
engagement |
Optional integrity-neutral note, 1 to 500 characters. Null when omitted. |
source |
Producer id. The vault sends vault-agent. |
sourceReferenceId |
Optional producer execution id. |
generatedAt |
Instant the producer wrote the verdict, RFC 3339. |
status |
Record status. A new row is active. |
id, dateCreated, dateLastModified |
Server-assigned. |
sessionStartedAt, sessionEndedAt |
Session window in UTC. |
timeZone |
School IANA zone for that session. |
Clock values inside why and evidence are bare HH:MM. They carry no date and no zone. The date comes from sessionStartedAt and sessionEndedAt. The zone name comes from timeZone. Those clock values are UTC today. Do not read 09:05 in the sample as 09:05 in America/Chicago.
Step 1: Store a verdict
POST /insights/1.0/sessions/{sessionId}/integrity-verdict
Requires events.write. A later write for the same session replaces the row.
Request body (RED):
{
"verdict": "RED",
"tags": ["Unauthorized help from a Device"],
"why": "A phone was in frame during the scored questions.",
"evidence": "NoDevices insight 09:05-09:06.",
"source": "vault-agent",
"generatedAt": "2026-03-10T09:31:00.000Z"
}
Response 200:
{
"id": "11111111-1111-4111-8111-111111111111",
"sessionId": "00000000-0000-4000-8000-000000000001",
"verdict": "RED",
"greyReason": null,
"tags": ["Unauthorized help from a Device"],
"why": "A phone was in frame during the scored questions.",
"evidence": "NoDevices insight 09:05-09:06.",
"engagement": null,
"source": "vault-agent",
"sourceReferenceId": null,
"generatedAt": "2026-03-10T09:31:00.000Z",
"status": "active",
"dateCreated": "2026-03-10T09:31:01.000Z",
"dateLastModified": "2026-03-10T09:31:01.000Z",
"sessionStartedAt": "2026-03-10T08:59:00.000Z",
"sessionEndedAt": "2026-03-10T09:30:00.000Z",
"timeZone": "America/Chicago"
}
id, status, dateCreated, and dateLastModified are server-assigned. The evidence window 09:05-09:06 sits inside the session window 08:59Z to 09:30Z. It is still UTC. On 2026-03-10 that is 04:05 in America/Chicago, not 09:05 local.
Request body (GREY):
{
"verdict": "GREY",
"greyReason": "No Attempt",
"tags": [],
"why": "The student left before answering a question.",
"evidence": "No scored question between 09:00-09:02.",
"source": "vault-agent",
"generatedAt": "2026-03-10T09:32:00.000Z"
}
Step 2: Read one session
GET /insights/1.0/sessions/{sessionId}/integrity-verdict
Requires events.readonly. A stored row returns the same shape as the write response.
A known session with no row returns 200 with verdict: null:
{
"sessionId": "00000000-0000-4000-8000-000000000001",
"verdict": null,
"sessionStartedAt": "2026-03-10T08:59:00.000Z",
"sessionEndedAt": "2026-03-10T09:30:00.000Z",
"timeZone": "America/Chicago"
}
That is not GREEN. An unknown or revoked session returns 404.
Step 3: Read a test-level roll-up
GET /insights/1.0/integrity-verdicts?sessionId={id1},{id2}
Requires events.readonly. The query takes 1 to 64 comma-separated session ids. All named sessions must belong to one student.
The roll-up is computed at read. It is not stored.
- Sessions with no row still appear in
sessionsasverdict: null. They drop out of the computedrollup. - If every judged session is
GREY, the roll-up isGREY.greyReasonis that shared reason, ornullwhen the reasons differ. - Otherwise rank is
REDoverORANGEoverGREEN.GREYdoes not compete. decidingSessionIdsare the sessions that hold the winning verdict.tagsare those sessions' tags, de-duplicated in encounter order.engagementscome from every non-grey judged session that has an engagement note, not only the deciding sessions.rollupisnullwhen no named session has a row.
Example. One RED session and one GREEN session:
{
"sessions": [
{
"id": "11111111-1111-4111-8111-111111111111",
"sessionId": "00000000-0000-4000-8000-000000000001",
"verdict": "RED",
"greyReason": null,
"tags": ["Unauthorized help from a Device"],
"why": "A phone was in frame during the scored questions.",
"evidence": "NoDevices insight 09:05-09:06.",
"engagement": null,
"source": "vault-agent",
"sourceReferenceId": null,
"generatedAt": "2026-03-10T09:31:00.000Z",
"status": "active",
"dateCreated": "2026-03-10T09:31:01.000Z",
"dateLastModified": "2026-03-10T09:31:01.000Z",
"sessionStartedAt": "2026-03-10T08:59:00.000Z",
"sessionEndedAt": "2026-03-10T09:30:00.000Z",
"timeZone": "America/Chicago"
},
{
"id": "22222222-2222-4222-8222-222222222222",
"sessionId": "00000000-0000-4000-8000-000000000002",
"verdict": "GREEN",
"greyReason": null,
"tags": [],
"why": "No integrity finding in this sitting.",
"evidence": "Clean window 09:10-09:20.",
"engagement": null,
"source": "vault-agent",
"sourceReferenceId": null,
"generatedAt": "2026-03-10T09:33:00.000Z",
"status": "active",
"dateCreated": "2026-03-10T09:33:01.000Z",
"dateLastModified": "2026-03-10T09:33:01.000Z",
"sessionStartedAt": "2026-03-10T09:08:00.000Z",
"sessionEndedAt": "2026-03-10T09:25:00.000Z",
"timeZone": "America/Chicago"
}
],
"rollup": {
"verdict": "RED",
"greyReason": null,
"tags": ["Unauthorized help from a Device"],
"decidingSessionIds": ["00000000-0000-4000-8000-000000000001"],
"engagements": []
}
}
engagements is the list of engagement notes from every non-grey judged session, each with its sessionId.
Step 4: Receive a verdict without polling
Subscribe to insights_session_integrity_verdict.ready. It fires when a stored verdict becomes readable on GET /insights/1.0/sessions/{sessionId}/integrity-verdict. That includes the first write and a later write that replaces the row.
There is one row per session. A second delivery for the same sessionId means the stored verdict changed. Refetch. Do not treat the event as insert-only.
The payload is { sessionId } only. The verdict, its tags, its reason, and its evidence are not in the payload. Fetch them with the GET in Step 2.
Your subscription receives the event only when that GET would succeed with your credentials.
See Webhooks — Session Integrity Verdict Events for the payload, routing, signature verification, and retries.
Errors
| Status | When |
|---|---|
200 with verdict: null |
The session exists and has no row. |
400 |
Tags do not match the family. greyReason is missing on GREY, or set on any other verdict. A roll-up names sessions of more than one student. sessionId is missing, empty, or longer than 64 ids. |
403 |
The caller lacks organization access. |
404 |
The session is unknown or revoked. A roll-up names a missing id. |
