Signals Insights

Report an insight your system judged, such as a student playing a game during a lesson, so TimeBack stores it and shows it alongside its own.

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

What a reported insight is

An insight is one judgment about what a student did during one session, over a span of time. TimeBack's own rules produce some insights. A trusted reporter, such as a video-analysis service, can report others through this API. Reported insights are what this guide covers.

Field Meaning
type, version What was judged, for example WasteGames version v1. The type must be registered, at its registered version
tbSessionId The session it happened in. The student is the session's student
startedAt When it started
endedAt When it ended. Defaults to startedAt until you send it
reason A short description a parent could read
severity red reaches the school safety officer; orange is stored and counted but hidden. Optional

The reporter is the API client whose token reported the insight. Only that client can change it.

GET /signals/1.0/insights/types lists the registered types. TimeBack sets each type's version; ask for the versions of the types you report when you request access.

Getting access

  1. Register your App and obtain OAuth credentials. See Level 0: Register Your App and Authentication.
  2. Request the Signals scope for your M2M client: https://timeback-platform.trilogy.com/signals/scope/insights.write.
  3. Request the permission signals:report_insight:all, granted to your client's tenant on urn:platform. It covers every registered type. Without it, reporting returns 403; once it is granted, the same request succeeds with no change on your side.

Ask for the scope and the permission through Getting access.

Every call in this guide is made with your client's own token, from the client-credentials grant. A token for a person, or one delegated to a person, carries your client's id too, but it is refused with 403.

Endpoint

Method Path Scope Purpose
PUT /signals/1.0/insights/{insightId} insights.write Report an insight, or move its start and its end

Report an insight

1. Generate the insight id

You choose the id: a UUID you generate and keep. Sending the same id again lands on the same insight, so a request that timed out is safe to repeat. One id is always one insight: TimeBack never joins two ids into one, even when their spans touch.

2. Send the insight

const insightId = crypto.randomUUID();
const response = await fetch(`https://platform.timeback.com/signals/1.0/insights/${insightId}`, {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    type: 'WasteGames',
    version: 'v1',
    tbSessionId: sessionId,
    startedAt: '2026-10-29T10:00:00.000Z',
    reason: 'Playing a browser game in another tab.',
  }),
});
Field Type Required Description
type string Yes A registered insight type
version string Yes The type's registered version
tbSessionId string Yes The session it happened in
startedAt date-time Yes When it started
endedAt date-time No When it ended. Not before startedAt. Defaults to startedAt
reason string No 1 to 5000 characters. Defaults to Reported by consumer
severity string No red or orange

The body takes only these fields: anything else returns 400. Date-times are ISO 8601 with Z or an offset.

A 200 returns the insight as stored:

{
  "id": "2b7e4c1a-9d3f-4a6e-8b52-1f0c7d9e3a48",
  "type": "WasteGames",
  "version": "v1",
  "tbSessionId": "urn:uuid:7f1c2a40-6b0e-4d8a-9c3e-2f5d6a7b8c9d",
  "startedAt": "2026-10-29T10:00:00.000Z",
  "endedAt": "2026-10-29T10:00:00.000Z",
  "reason": "Playing a browser game in another tab.",
  "severity": null,
  "rolloutState": "NOTIFY",
  "outcome": "raised",
  "dateCreated": "2026-10-29T10:00:01.000Z",
  "dateLastModified": "2026-10-29T10:00:01.000Z"
}

Retries and updates

  • A PUT to the same id can change startedAt, endedAt, reason and severity.
  • A field you leave out keeps its stored value. So a late retry of your first call, without endedAt, does not undo an end you already sent.
  • type, version and tbSessionId never change under an id. Sending different ones returns 409. An insight keeps the version it was reported at, even after TimeBack moves the type to a new one.
  • An id another client reported, or the id of an insight TimeBack's rules produced, returns 403.
  • There is no delete. An insight invalidated through Signals Feedback stays invalidated: a later PUT does not raise it again.

Move the start and the end

Report an episode when it starts, then send its end when it is over:

await fetch(`https://platform.timeback.com/signals/1.0/insights/${insightId}`, {
  method: 'PUT',
  headers: {
    Authorization: `Bearer ${accessToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    type: 'WasteGames',
    version: 'v1',
    tbSessionId: sessionId,
    startedAt: '2026-10-29T10:00:00.000Z',
    endedAt: '2026-10-29T10:04:00.000Z',
  }),
});

Send a later endedAt again to extend an episode that is still going, or a different startedAt to correct the start.

Point-in-time insights

An insight with no duration, such as a journal entry, is one PUT with startedAt only. Its endedAt is the same instant.

What happens after you report

TimeBack checks every reported insight the same way it checks its own, on every PUT:

  • Session. An Academic Integrity insight on a standard (not proctored) session is stored as suppressed.
  • Minimum duration. Some types count only once they last long enough. A shorter insight is stored as suppressed and becomes raised when a later endedAt makes it long enough. This is why an episode reported at its start can be suppressed until you close it.
  • Rollout. rolloutState is the stage the student's school was in for that type when you first reported it: SHADOW, NOTIFY or ENFORCE. It does not change on later calls.

Some types are judged only by TimeBack's own rules, such as AcademicIntegrityStayInTheTest and ObservabilityCameraWorking; reporting them returns 400, as does a type past its sunset date.

A raised insight whose rolloutState is not SHADOW shows in the Signals reads, such as GET /signals/1.0/sessions/{sessionId}/insights and GET /signals/1.0/users/{userId}/insights, with rule set to Reported. Subscribers receive insight.created when it first shows, insight.updated when a later PUT changes it, and insight.deleted when a later PUT makes it suppressed, for example by moving its end back below the type's minimum duration; see Webhooks. A suppressed or SHADOW insight is stored but not shown.

Worked example: an episode reported at its start

A video-analysis service sees a student open a browser game at 10:00 during a lesson.

  1. Start. It generates 2b7e4c1a-… and sends the PUT from Send the insight with startedAt 10:00. The response has endedAt 10:00 and outcome: raised; the school's stage for WasteGames is NOTIFY, so the insight shows in the student's session at once and subscribers receive insight.created.
  2. Retry. The service's queue redelivers the same call. The response is the same insight, unchanged, and no new webhook is sent.
  3. End. At 10:04 the student closes the game. The service sends the same id with endedAt 10:04, as in Move the start and the end. The insight now lasts four minutes, and subscribers receive insight.updated.

Errors

Status When
400 The body fails validation: an unknown field, an unknown type, a new insight at a version other than the type's registered one, a type TimeBack judges itself, a type past its sunset, an unknown session, or endedAt before startedAt
401 The token is missing, expired, or lacks insights.write
403 Your tenant lacks signals:report_insight:all, the token is a person's or delegated to a person, or another client reported the id
409 The call changes type, version or tbSessionId of an existing id

Error bodies carry error and message. A validation 400 also lists each failing input in fields.