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.secondsByClassholds the seven classes, and their values sum tobaseSeconds.suspectedSecondscounts 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
- Register your App and obtain OAuth credentials. See Level 0: Register Your App and Authentication.
- Request the read scope for your M2M client:
https://purl.imsglobal.org/spec/caliper/v1p2/scope/events.readonly. Backfill also needshttps://timeback-platform.trilogy.com/signals/scope/insights.readonly. - 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}/subscriptionswithwebhooks.write, from a client whose tenant holds theapp:manage_webhooksgrant 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:
- Page
GET /signals/1.0/sessionswithorgId,after, andbefore, usinglimit(up to 100) andoffset. This needsinsights.readonly. - For each session, call Read one session.
adjustment: nullmeans 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
adjustedSecondswithbaseSeconds, andadjustedXpwithbaseXp. - Store
ruleVersionandcomputedAtnext 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 newreadyevent. - 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:
baseXpsums the session's ledger entries, excluding manual adjustments and deleted entries.adjustedXpis0whenintegrityStatusisCONFIRMED. Otherwise it equalsbaseXp. Wasted time never reduces XP.xpAdjustmentisadjustedXpminusbaseXp.
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.
