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 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. In the sample below, 09:05 is 09:05 UTC. On 2026-03-10 that is 04:05 in America/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 not GREEN.
  • 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

  1. Register your App and obtain OAuth credentials. See Level 0: Register Your App and Authentication.

  2. 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
  3. 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 Person
  • Unauthorized help from an Application
  • Unauthorized help from a Device

ORANGE takes one or more of:

  • Suspected help from a Person
  • Suspected help from an Application
  • Suspected help from a Device
  • Suspicious 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 sessions as verdict: null. They drop out of the computed rollup.
  • If every judged session is GREY, the roll-up is GREY. greyReason is that shared reason, or null when the reasons differ.
  • Otherwise rank is RED over ORANGE over GREEN. GREY does not compete.
  • decidingSessionIds are the sessions that hold the winning verdict.
  • tags are those sessions' tags, de-duplicated in encounter order.
  • engagements come from every non-grey judged session that has an engagement note, not only the deciding sessions.
  • rollup is null when 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.