Level 4: Insights API
Access coaching insights generated from student learning activity to power dashboards, reports, and intervention workflows.
What Insights Provides
The Insights API gives you read access to coaching insights that TimeBack generates from Caliper events (Level 3). By integrating with Insights, you enable:
- Student behavior dashboards showing time-on-task and waste patterns
- Session-level analytics with detailed insight breakdowns
- Trend analysis over configurable time ranges
- Cross-session aggregations for progress tracking
Without the Insights API, you can send learning events but can't programmatically access the insights TimeBack generates. With Insights, you can build custom dashboards and integrate coaching data into your app's existing analytics.
For aggregate reporting across students and orgs, see Analytics. To decide between the REST API, an MCP server, and webhooks for a given job, see Choosing an Integration Surface.
Prerequisites
Before implementing Insights:
- Implement Level 3 (Caliper Events)—insights are generated from Caliper events you send
- Request OAuth credentials with the
events.readonlyscope (see Authentication Guide)
API Endpoints
The Insights API provides these endpoints for accessing coaching data:
| Endpoint | Description |
|---|---|
GET /insights/1.0/users/{userId} |
Get insights for a specific user |
GET /insights/1.0/users/{userId}/sessions |
Get session list for a user |
GET /insights/1.0/sessions/{sessionId} |
Get insights for a specific session |
GET /insights/1.0/users/{userId}/overview |
Get aggregated trend and breakdown data |
GET /insights/1.0/sessions |
Get sessions across an organization, enriched with waste metrics and type counts |
GET /insights/1.0/types |
Get the insight type catalog visible to your credentials, optionally by category |
GET /insights/1.0/sessions/{sessionId}/enforcement-state |
Get live strike counts, active lockout, and required action for a proctored session |
The first four cover reporting and dashboards and are the subject of the sections below. The last three support type discovery and in-session enforcement; see Insight Types.
Authentication
All Insights endpoints require OAuth 2.0 authentication. Request the events read scope:
scope: https://purl.imsglobal.org/spec/caliper/v1p2/scope/events.readonly
Include your access token in requests:
const response = await fetch(`https://platform.timeback.com/insights/1.0/users/${userId}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
});
Get User Insights
Retrieve insights for a specific user with optional filtering and pagination.
GET /insights/1.0/users/{userId}
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
applicationId |
uuid | No | Filter insights to a specific learning app |
after |
datetime | No | Filter insights after this timestamp (inclusive) |
before |
datetime | No | Filter insights before this timestamp (inclusive) |
limit |
integer | No | Maximum items to return (1-100, default 20) |
offset |
integer | No | Number of items to skip (default 0) |
type |
string | No | Comma-separated insight type slugs to filter by. Overrides category and default. |
category |
string | No | Comma-separated categories (e.g., PhysicalEnvironment,OnlineEnvironment). Overrides default. |
Example Request
const userId = 'student-platform-id'; // From LTI 'sub' or roster lookup
const response = await fetch(
`https://platform.timeback.com/insights/1.0/users/${userId}?limit=10&after=2025-01-01T00:00:00Z`,
{
headers: { Authorization: `Bearer ${accessToken}` },
},
);
const data = await response.json();
Example Response
{
"session": {
"startedAtTime": "2025-01-15T09:00:00Z",
"endedAtTime": "2025-01-15T10:30:00Z",
"durationInSeconds": 5400,
"wasteDurationInSeconds": 720,
"wastePercentage": 13
},
"insights": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"insightType": "PhysicalVerbalSocializing",
"reason": "Conversation detected during a learning block",
"startedAtTime": "2025-01-15T09:15:00Z",
"endedAtTime": "2025-01-15T09:20:00Z",
"durationInSeconds": 300,
"version": "v1"
}
],
"offset": 0,
"limit": 10,
"total": 1
}
Get User Sessions
Retrieve sessions for a user with optional filtering.
GET /insights/1.0/users/{userId}/sessions
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
applicationId |
uuid | No | Filter sessions to a specific learning app |
startedAfter |
datetime | No | Lower bound for session start time (inclusive) |
startedBefore |
datetime | No | Upper bound for session start time (inclusive) |
category |
string | No | Comma-separated categories (e.g., PhysicalEnvironment,OnlineEnvironment). Overrides default. |
isProctored |
boolean | No | Filter by proctored status. Also selects proctoring types when category is not provided. |
limit |
integer | No | Maximum items to return (1-100, default 20) |
offset |
integer | No | Number of items to skip (default 0) |
Example Request
const response = await fetch(
`https://platform.timeback.com/insights/1.0/users/${userId}/sessions?startedAfter=2025-01-01T00:00:00Z`,
{
headers: { Authorization: `Bearer ${accessToken}` },
},
);
const data = await response.json();
Example Response
{
"sessions": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"applicationId": "learning-app-uuid",
"startedAtTime": "2025-01-15T09:00:00Z",
"endedAtTime": "2025-01-15T10:30:00Z",
"durationInSeconds": 5400,
"wasteDurationInSeconds": 720,
"wastePercentage": 13,
"isProctored": true,
"proctoredResult": "PASSED",
"recordingStartedAtTime": "2025-01-15T09:00:05Z",
"recordingEndedAtTime": "2025-01-15T10:29:55Z",
"recordingS3Key": "recordings/2025/01/15/session-550e8400.webm"
}
],
"offset": 0,
"limit": 20,
"total": 1
}
Get Session Insights
Retrieve insights for a specific session.
GET /insights/1.0/sessions/{sessionId}
When no session row exists yet for the given sessionId (for example,
immediately after a session starts but before the asynchronous Caliper
ingestion pipeline has materialized it), this endpoint returns 200
with an empty result (insights: [], total: 0, and a default session
summary with zero duration). Polling clients should treat the empty
result as "no data yet" rather than as an error.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
sessionId |
uuid | Yes | Session ID from the sessions list |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
isProctored |
boolean | No | Filter insights by proctored status. Also selects proctoring types when category is not provided. |
limit |
integer | No | Maximum items to return (1-100, default 20) |
offset |
integer | No | Number of items to skip (default 0) |
type |
string | No | Comma-separated insight type slugs to filter by. Overrides category and default. |
category |
string | No | Comma-separated categories (e.g., PhysicalEnvironment,OnlineEnvironment). Overrides default. |
Example Request
const sessionId = '550e8400-e29b-41d4-a716-446655440000';
const response = await fetch(`https://platform.timeback.com/insights/1.0/sessions/${sessionId}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
const data = await response.json();
Get User Insights Overview
Retrieve aggregated overview data including trends and breakdowns for a time range.
GET /insights/1.0/users/{userId}/overview
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
startedAfter |
datetime | Yes | Lower bound for time window (inclusive) |
startedBefore |
datetime | Yes | Upper bound for time window (exclusive) |
targetTimezone |
string | Yes | IANA timezone for local-day bucketing |
Example Request
const params = new URLSearchParams({
startedAfter: '2025-01-01T00:00:00Z',
startedBefore: '2025-01-08T00:00:00Z',
targetTimezone: 'America/New_York',
});
const response = await fetch(`https://platform.timeback.com/insights/1.0/users/${userId}/overview?${params}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
const data = await response.json();
Example Response
{
"summary": {
"totalDurationInSeconds": 36000,
"totalWasteDurationInSeconds": 5400,
"wastePercentage": 15,
"biggestInsight": {
"level": "OnDeviceDistractions",
"durationInSeconds": 2400,
"wastePercentage": 7
}
},
"trend": {
"buckets": [
{
"startedAtTime": "2025-01-01T00:00:00Z",
"endedAtTime": "2025-01-02T00:00:00Z",
"totalDurationInSeconds": 7200,
"wasteDurationInSeconds": 1080,
"wastePercentage": 15,
"levels": [
{ "level": "EnvironmentalDistractions", "durationInSeconds": 300 },
{ "level": "OnDeviceDistractions", "durationInSeconds": 780 }
]
}
]
},
"breakdown": {
"levels": [
{
"level": "OnDeviceDistractions",
"durationInSeconds": 2400,
"wastePercentage": 7,
"subtypes": [{ "insightType": "OnlineAppFocusSwitch", "durationInSeconds": 2400 }]
},
{
"level": "EnvironmentalDistractions",
"durationInSeconds": 900,
"wastePercentage": 3,
"subtypes": [{ "insightType": "PhysicalVerbalSocializing", "durationInSeconds": 900 }]
}
]
}
}
Insight Types
GET /insights/1.0/types is the authoritative list of insight types available to your credentials. See the API Reference for the full operation. This guide explains what the categories mean and how the types are used — it does not restate the catalog as a fixed list of slugs.
GET /insights/1.0/types returns the global active registry visible to the authenticated caller. Results do not vary by organization. External callers receive only visibility=external types; internal callers also receive active internal types. Locale message bundles and organization policy do not participate in this endpoint. Waste Meter consumers select entries with isWaste=true from the response. On every create or update, precedenceLevel must satisfy the category rules (Engagement requires 1-5; Proctoring and Cheating require null). Creating or updating an externally visible waste type additionally requires a non-blank displayName and description, and a six-digit hex color. Catalog reads are cached in-process for up to 10 minutes, so registry changes become visible across warm Lambda containers within that bound (the writing process invalidates its own cache immediately).
The Supported Categories
Four categories are supported and actively developed. Each is scoped to a kind of session, so the categories you request follow from the sessions you care about:
| Category | What it covers | Where it applies |
|---|---|---|
AcademicIntegrity |
A rule of the test was broken: another person involved, another device or another window in use, or the test left or taken out of full screen. | Proctored sessions |
Observability |
The student cannot be seen or heard well enough to judge anything else: camera or microphone unavailable, lens covered, face out of frame, room too dark, or a degraded feed. | Every session, proctored or not |
PhysicalEnvironment |
Something in the space around the student pulled them off task, such as a conversation with someone nearby. | Non-proctored learning sessions |
OnlineEnvironment |
Something on the device pulled the student's attention away from the learning app, such as focus moving to another app or window. | Non-proctored learning sessions |
So a proctored test produces AcademicIntegrity and Observability insights, and a non-proctored learning session produces PhysicalEnvironment, OnlineEnvironment and Observability insights. Observability is the one category that spans both, and what it means for the student differs by context: during a learning block it is a prompt to fix the setup, during a proctored test it is the reason the session cannot be proctored. See In-Session Enforcement for what the platform does with it.
Select by category, then resolve the types from the catalog. The slugs named in this guide illustrate what each category contains; they are not a list to compile into your product. Individual types are added, renamed and retired between releases, and a rename appears as a new slug alongside a retired one rather than as an alias. GET /insights/1.0/types is the only current answer to which slugs sit inside a category. See How Types Change.
Categories outside that set fall into three groups. Proctoring and Cheating are deprecated; see Deprecated Insight Types. Safety, Stability and Telemetry are internal to TimeBack and never appear in external responses. Journal carries narrative entries describing what happened in a session, as text rather than as wasted time. The catalog endpoint is authoritative about what your credentials receive.
Default Behavior and Type Selection
The default filter returns the deprecated set, not the supported set. A caller that passes neither category nor type receives Engagement-category types only. That default is frozen for backward compatibility with integrations built before the categories above existed. A new integration passes category explicitly.
| Params passed | Types returned | Example use case |
|---|---|---|
?category=AcademicIntegrity,Observability,OnlineEnvironment,PhysicalEnvironment |
The full supported set | What a new integration sends |
?category=PhysicalEnvironment |
One supported category | Single-category view |
?type=PhysicalVerbalSocializing,OnlineAppFocusSwitch |
Exact slugs only | Custom type selection |
| (none) | Engagement-category types only | Legacy default, deprecated set |
?isProctored=true |
Proctoring types only | Legacy proctoring tab |
Precedence: type > category > default.
The wasteDurationInSeconds and wastePercentage fields reflect the intersection of the fetched types and the types carrying isWaste = true, so the category filter you pass determines what waste means in the response. Within the supported set, waste comes from OnlineEnvironment and PhysicalEnvironment; AcademicIntegrity and Observability types are not waste and contribute no duration to those fields. Two endpoints are exceptions. Org-level enriched sessions (GET /sessions) cover every isWaste = true type regardless of filter, and the overview endpoint accepts no type or category parameter at all — it always spans every waste type. The latter also means AcademicIntegrity and Observability insights never appear in overview data, since neither category is waste.
Reading the Catalog
Call the type catalog to discover which types your credentials can retrieve:
GET /insights/1.0/types?category=AcademicIntegrity,Observability,OnlineEnvironment,PhysicalEnvironment
Example response (field shapes are complete; which types appear depends on your credentials and filters):
[
{
"id": "d66e396d-8842-404e-83f9-2881bea8f29d",
"slug": "PhysicalVerbalSocializing",
"displayName": "Verbal Socializing",
"category": "PhysicalEnvironment",
"visibility": "external",
"isContinuous": false,
"precedenceLevel": 3,
"isWaste": true,
"description": "Socializing is when you're part of a conversation during a learning session.",
"color": null,
"triggersInSessionNotification": false,
"detectionSources": ["webcam"]
},
{
"id": "fcdeabd6-6a5e-486e-969e-dd7b477bccab",
"slug": "AcademicIntegrityFullScreenRequired",
"displayName": "Full screen required",
"category": "AcademicIntegrity",
"visibility": "external",
"isContinuous": false,
"precedenceLevel": null,
"isWaste": false,
"description": null,
"color": null,
"triggersInSessionNotification": true,
"detectionSources": ["client"]
}
]
Type Metadata
Each insight type returned by GET /insights/1.0/types has the following properties:
| Field | Type | Description |
|---|---|---|
id |
string (uuid) | Stable identifier for the type |
slug |
string | Unique PascalCase identifier (e.g., PhysicalVerbalSocializing) |
displayName |
string | Human-readable name |
category |
string | Category name. See The Supported Categories |
visibility |
string | external or internal. The endpoint only returns types your credentials may see, so an external integrator observes only external |
isContinuous |
boolean | Whether this type supports continuous gap-based merging |
precedenceLevel |
number | null | Precedence for waste overlap resolution (1=highest, 5=lowest, null if not applicable) |
isWaste |
boolean | Whether this type counts toward waste calculations |
description |
string | null | Human-readable description for tooltips |
color |
string | null | Hex color for UI rendering (e.g., #FF5722) |
triggersInSessionNotification |
boolean | Whether instances of this type surface as in-session notifications to the student |
detectionSources |
string[] | Semantic medium(s) analyzed to mine this type: webcam, screen, client, dom, network, app_events. Empty means no known medium dependency |
How Types Change
Insight types are added, renamed, and retired between releases. A retired type stops appearing in GET /insights/1.0/types and stops producing new insights. Insights already recorded against a retired type remain readable. Nothing on the response announces that a type was retired.
Build integrations that survive those changes:
- Read the catalog at runtime and cache it with a TTL — do not compile a fixed list of slugs into your product.
- Render
displayName,description, andcolorfrom the catalog rather than from a local lookup table. - Treat an unrecognized slug as displayable, never as an error.
- Do not write an exhaustive
switchover slugs with a throwing default. - Derive waste from
isWasterather than from a hard-coded slug list.
In-Session Enforcement
AcademicIntegrity and Observability types do more than appear in reporting: they drive what the student sees and is asked to do during the session.
How enforcement works. An organization sets a policy per insight type with a rollout state of SHADOW, NOTIFY, or ENFORCE. When a session starts under a policy, that policy set is frozen onto the session — mid-session policy edits do not change its outcome. A type classed STUDENT_CONTROLLABLE under ENFORCE accrues a strike against the student; a TECHNICAL type can show the student a message but never accrues a strike. Strikes past the organization's warning budget open a lockout, and the session's required action moves from NONE to RESUME to END_SESSION.
Read that state with GET /insights/1.0/sessions/{sessionId}/enforcement-state (strike counts, active lockout, pending notifications, and required action). The endpoint returns 409 Conflict for a session that was never pinned under a policy.
Deprecated Insight Types
The Engagement, Proctoring, and Cheating categories are deprecated. They are no longer maintained, they will stop being populated, and no new integration should build against them.
Two consequences matter in practice. Insights already recorded against these categories stay readable, so a dashboard rendering historical data keeps working. And the endpoints still return them under the default filter, which is why a supported integration passes category explicitly. See Default Behavior and Type Selection.
GET /insights/1.0/types remains authoritative for what your credentials can retrieve. See How Types Change for how to build an integration that survives retirement.
Overview Levels
Overview data groups insights into six levels:
| Level | Description |
|---|---|
StudentNotPresent |
Student away from seat or eyes off screen |
FocusAndIntensity |
Reduced focus or intensity during learning (for example, idling) |
EnvironmentalDistractions |
External distractions (eating, socializing) |
OnDeviceDistractions |
Digital distractions (games, social media) |
LearningAppBestPractices |
Learning behavior issues (skipping, shopping) |
Unclassified |
Insight time that maps to no other level — render it rather than dropping it |
Looking Up Application Information
Insights and sessions include an applicationId field that identifies which learning app the data came from. To map application IDs to application names and details, use the Applications API.
Get All Applications
GET /applications/1.0
This endpoint returns all applications. Each application's sourcedId is the applicationId you'll see in insights responses.
Required scope: https://purl.imsglobal.org/spec/lti/v1p3/scope/lti.readonly
This endpoint supports filtering, pagination, sorting, and field selection. See OneRoster API Conventions for details.
Example Request
const response = await fetch('https://platform.timeback.com/applications/1.0', {
headers: { Authorization: `Bearer ${accessToken}` },
});
const data = await response.json();
Example Response
{
"applications": [
{
"sourcedId": "app-uuid",
"name": "Math Learning Suite",
"description": "Comprehensive math learning platform",
"logoUrl": "https://example.com/logo.png",
"applicationType": "learning_app",
"wasteMeter": "on",
"proctoringMode": "off"
}
],
"offset": 0,
"limit": 10,
"total": 1
}
Building an Application Lookup Map
Cache application information to efficiently display app names in your dashboards:
interface ApplicationInfo {
name: string;
logoUrl: string;
}
async function buildApplicationLookup(accessToken: string): Promise<Map<string, ApplicationInfo>> {
const response = await fetch('https://platform.timeback.com/applications/1.0?limit=100', {
headers: { Authorization: `Bearer ${accessToken}` },
});
const data = await response.json();
const applicationMap = new Map<string, ApplicationInfo>();
for (const app of data.applications) {
applicationMap.set(app.sourcedId, {
name: app.name,
logoUrl: app.logoUrl,
});
}
return applicationMap;
}
// Usage: Display application name for a session
const applicationLookup = await buildApplicationLookup(accessToken);
const session = sessions[0];
const applicationInfo = applicationLookup.get(session.applicationId);
console.log(`Session in: ${applicationInfo?.name}`); // "Session in: Math Learning Suite"
Common Use Cases
Building a Student Dashboard
Use the overview endpoint to show weekly trends:
async function getWeeklyOverview(userId: string, timezone: string): Promise<Overview> {
const now = new Date();
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
const params = new URLSearchParams({
startedAfter: weekAgo.toISOString(),
startedBefore: now.toISOString(),
targetTimezone: timezone,
});
const response = await fetch(`https://platform.timeback.com/insights/1.0/users/${userId}/overview?${params}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
return response.json();
}
Listing Recent Sessions with Waste Stats
async function getRecentSessions(userId: string): Promise<Session[]> {
const response = await fetch(`https://platform.timeback.com/insights/1.0/users/${userId}/sessions?limit=10`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
const data = await response.json();
return data.sessions;
}
Fetching All Supported Categories for a Session
const SUPPORTED_CATEGORIES = 'AcademicIntegrity,Observability,OnlineEnvironment,PhysicalEnvironment';
async function getAllInsights(sessionId: string): Promise<Insights> {
const response = await fetch(
`https://platform.timeback.com/insights/1.0/sessions/${sessionId}?category=${SUPPORTED_CATEGORIES}`,
{ headers: { Authorization: `Bearer ${accessToken}` } },
);
return response.json();
}
Drilling into Session Details
async function getSessionDetails(sessionId: string): Promise<Insights> {
const response = await fetch(`https://platform.timeback.com/insights/1.0/sessions/${sessionId}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
return response.json();
}
Error Handling
The API returns standard HTTP status codes:
| Status | Description |
|---|---|
| 200 | Success |
| 400 | Bad request (invalid parameters) |
| 401 | Unauthorized (missing or invalid token) |
| 403 | Forbidden (insufficient scope) |
| 404 | Not found (user doesn't exist) |
| 500 | Internal server error |
Error responses include details:
{
"error": "Bad request",
"message": "Invalid UUID format for userId"
}
Related Docs
Level 3: Caliper Events
Insights are generated from Caliper events. Implement Level 3 first to start generating insights.
Session Management
Understand session lifecycle, auto-attach vs explicit sessions, heartbeat, and session metadata.
Authentication
Obtain OAuth client credentials and access tokens required for the Insights API.
OneRoster API Conventions
Learn about filtering, pagination, sorting, and field selection for the Applications API.
