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:

  1. Implement Level 3 (Caliper Events)—insights are generated from Caliper events you send
  2. Request OAuth credentials with the events.readonly scope (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:

  1. Read the catalog at runtime and cache it with a TTL — do not compile a fixed list of slugs into your product.
  2. Render displayName, description, and color from the catalog rather than from a local lookup table.
  3. Treat an unrecognized slug as displayable, never as an error.
  4. Do not write an exhaustive switch over slugs with a throwing default.
  5. Derive waste from isWaste rather 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"
}

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.