Session Adjustments

Read a session's adjusted time and adjusted XP, get notified when it changes, and backfill history.

Preview: a session's adjusted time is computed by a port of the StudyFilm feed's daily arithmetic. Values may change until a production parity check against the feed is accepted. Until then the feed's daily reports stay the reference for schools.

Note: This guide covers the integration flow. See the API Reference for complete endpoint documentation, all parameters, and response schemas.

What an adjustment is

A session's base time is split into seven classes. Wasted time and confirmed academic-integrity time are deducted. What remains is the adjusted time:

  • adjustedSeconds = baseSeconds - deductedSeconds.
  • secondsByClass holds the seven classes, and their values sum to baseSeconds.
  • suspectedSeconds counts time under suspected, unconfirmed integrity spans. It is reported, never deducted.

Values are whole seconds. Divide by 60 for minutes.

The platform keeps one record per session. A later write for the same session replaces it.

Each record also says why time was deducted (rationale), where the deducted time sat (deductedIntervals), and how much of the session's XP stands (xp).

Prerequisites

  1. Register your App and obtain OAuth credentials. See Level 0: Register Your App and Authentication.
  2. Request the read scope for your M2M client: https://purl.imsglobal.org/spec/caliper/v1p2/scope/events.readonly. Backfill also needs https://timeback-platform.trilogy.com/signals/scope/insights.readonly.
  3. Hold organization access to the session's organization. A caller without it receives 403.

Readers never need events.write. Only the platform's producer writes records; see Producer write.

Get notified

Subscribe to session_adjustment.ready. Every write emits it with { sessionId } only, so fetch the record with Read one session. A second delivery for the same session means the record was replaced, so refetch.

You can subscribe in either of two ways:

  • REST: POST /webhooks/1.0/{sourcedApplicationId}/subscriptions with webhooks.write, from a client whose tenant holds the app:manage_webhooks grant on the App.
  • Developer-platform MCP: devplatform_create_webhook_subscription, as the developer who registered the App. That developer already holds the grant.

See Webhooks for delivery format, signatures, and retries.

Read one session

GET /session/1.0/sessions/{sessionId}/adjustment

Requires events.readonly. A stored record returns:

{
  "sessionId": "00000000-0000-4000-8000-000000000001",
  "adjustment": {
    "id": "11111111-1111-4111-8111-111111111111",
    "baseSeconds": 1200,
    "adjustedSeconds": 960,
    "deductedSeconds": 240,
    "suspectedSeconds": 0,
    "secondsByClass": {
      "PRODUCTIVE": 840,
      "LOW_PRODUCTIVITY": 0,
      "SCHOOL": 120,
      "SYSTEM_FAILURE": 0,
      "WASTED": 240,
      "INSINCERE": 0,
      "UNOBSERVED": 0
    },
    "integrityStatus": "NOT_ESCALATED",
    "ruleVersion": "feed-f215c05",
    "source": "window-graph",
    "computedAt": "2026-03-10T09:35:00.000Z",
    "dateCreated": "2026-03-10T09:35:01.000Z",
    "dateLastModified": "2026-03-10T09:35:01.000Z",
    "rationale": {
      "wasteVerdict": "EFFECTIVE",
      "wasteExplanation": "The student spent roughly 4 minutes in a game. The rest of the session was on task.",
      "integrityExplanation": null
    },
    "deductedIntervals": [
      { "startTime": "2026-03-10T09:05:00.000Z", "endTime": "2026-03-10T09:09:00.000Z", "timeClass": "WASTED" }
    ],
    "xp": { "baseXp": 30, "adjustedXp": 30, "xpAdjustment": 0 }
  }
}

A known session with no record yet returns 200 with adjustment: null:

{
  "sessionId": "00000000-0000-4000-8000-000000000001",
  "adjustment": null
}

That means no adjustment has been computed yet. Use the session's raw duration until one arrives. An unknown or revoked session returns 404.

Backfill

There is no bulk endpoint for adjustments. To load history:

  1. Page GET /signals/1.0/sessions with orgId, after, and before, using limit (up to 100) and offset. This needs insights.readonly.
  2. For each session, call Read one session. adjustment: null means no adjustment has been computed yet.

Then keep the data current with the webhook.

Shadow mode

To compare adjusted time with what you credit today, without acting on it:

  • Per session, compare adjustedSeconds with baseSeconds, and adjustedXp with baseXp.
  • Store ruleVersion and computedAt next to every value you keep. The producer can recompute a session under a newer rule version, and the platform then replaces the record and sends a new ready event.
  • Refetch on every event, and keep the latest record per session.

Classes

Class Deducted
PRODUCTIVE No
LOW_PRODUCTIVITY No
SCHOOL No
SYSTEM_FAILURE No
WASTED Yes
INSINCERE Yes
UNOBSERVED No

INSINCERE is time covered by a confirmed academic-integrity violation. UNOBSERVED is time where nothing could be observed. The other classes are the StudyFilm window labels.

The producer decides which classes it deducts under its ruleVersion. deductedSeconds is the producer's total.

Integrity status

Value Meaning
CONFIRMED At least one confirmed academic-integrity violation.
SUSPECT Suspected, unconfirmed violations only.
CLEAN Reviewed and clean.
NOT_ESCALATED Screening found nothing to review.
UNJUDGED Review did not finish. Nothing is deducted for integrity.

Fields

Field Meaning
baseSeconds Session duration the producer classified.
adjustedSeconds baseSeconds minus deductedSeconds.
deductedSeconds Seconds removed from the base.
suspectedSeconds Seconds under suspected integrity spans. Never deducted.
secondsByClass All seven classes, each a non-negative integer, summing to baseSeconds.
integrityStatus One of the values above.
ruleVersion Version of the rules that produced the record, 1 to 64 characters.
source Producer id, 1 to 64 characters.
computedAt Instant the producer computed the record, RFC 3339.
wasteVerdict WASTED or EFFECTIVE. Optional; null when no verdict was produced.
wasteExplanation Plain-language reason for the verdict, 1 to 400 characters. Optional.
deductedIntervals Where the deducted time sat. Optional; see below.
id, dateCreated, dateLastModified Server-assigned.

Rationale

rationale carries wasteVerdict, wasteExplanation, and integrityExplanation. When integrityStatus is CONFIRMED, integrityExplanation is a fixed sentence: "Part of this session was deducted because academic-integrity evidence was confirmed." Otherwise it is null. For the detailed integrity reasoning, read GET /session/1.0/sessions/{sessionId}/integrity-report.

rationale is null on a record written without a verdict, an explanation, or intervals.

Deducted intervals

Each interval is { startTime, endTime, timeClass }, with RFC 3339 instants and timeClass WASTED or INSINCERE. Intervals are sorted by startTime and do not overlap. The durations of the WASTED intervals sum to secondsByClass.WASTED, and the INSINCERE intervals sum to secondsByClass.INSINCERE. An empty array means nothing was deducted. null means the record carries no intervals.

XP

xp is read from the XP ledger each time you read the record:

  • baseXp sums the session's ledger entries, excluding manual adjustments and deleted entries.
  • adjustedXp is 0 when integrityStatus is CONFIRMED. Otherwise it equals baseXp. Wasted time never reduces XP.
  • xpAdjustment is adjustedXp minus baseXp.

The adjustment is reported, not applied: the student's XP in the ledger does not change. Analysts with Analytics MCP access read the same baseXp, adjustedXp, and xpAdjustment per session from the session_adjustments view. Because XP is read live, an XP credit that arrives after the record changes baseXp without a new webhook event. Credits arriving more than 2 minutes after a session are rare and depend on the app.

Errors

Status Cause
400 secondsByClass does not sum to baseSeconds, is missing a class, or has an extra one. adjustedSeconds is not baseSeconds minus deductedSeconds. A value is negative or not an integer. An interval ends before it starts, intervals overlap or are unsorted, or their durations do not match secondsByClass. wasteExplanation is empty or longer than 400 characters. The body has an unknown field.
401 The token is missing, expired, or lacks the scope the endpoint needs.
403 The caller lacks organization access, or a student tries to write a record.
404 The session is unknown or revoked.

Producer write

Only the platform's adjustment producer writes records. Consumers never call this endpoint.

POST /session/1.0/sessions/{sessionId}/adjustment

Requires events.write. sessionId is the Caliper session id. A later write for the same session replaces the record and emits a new session_adjustment.ready event.

POST /session/1.0/sessions/{sessionId}/time-adjustment is a deprecated alias with the same body and response. Move to /adjustment; the alias will be removed.

Request body:

{
  "baseSeconds": 1200,
  "adjustedSeconds": 960,
  "deductedSeconds": 240,
  "suspectedSeconds": 0,
  "secondsByClass": {
    "PRODUCTIVE": 840,
    "LOW_PRODUCTIVITY": 0,
    "SCHOOL": 120,
    "SYSTEM_FAILURE": 0,
    "WASTED": 240,
    "INSINCERE": 0,
    "UNOBSERVED": 0
  },
  "integrityStatus": "NOT_ESCALATED",
  "ruleVersion": "feed-f215c05",
  "source": "window-graph",
  "computedAt": "2026-03-10T09:35:00.000Z",
  "wasteVerdict": "EFFECTIVE",
  "wasteExplanation": "The student spent roughly 4 minutes in a game. The rest of the session was on task.",
  "deductedIntervals": [
    { "startTime": "2026-03-10T09:05:00.000Z", "endTime": "2026-03-10T09:09:00.000Z", "timeClass": "WASTED" }
  ]
}

Response 200: the stored record, in the shape shown under Read one session.