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
- Register your App and obtain OAuth credentials. See Level 0: Register Your App and Authentication.
- Request the Signals scope for your M2M client:
https://timeback-platform.trilogy.com/signals/scope/insights.write. - Request the permission
signals:report_insight:all, granted to your client's tenant onurn:platform. It covers every registered type. Without it, reporting returns403; 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
PUTto the same id can changestartedAt,endedAt,reasonandseverity. - 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,versionandtbSessionIdnever change under an id. Sending different ones returns409. 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
PUTdoes 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
suppressedand becomesraisedwhen a laterendedAtmakes it long enough. This is why an episode reported at its start can besuppresseduntil you close it. - Rollout.
rolloutStateis the stage the student's school was in for that type when you first reported it:SHADOW,NOTIFYorENFORCE. 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.
- Start. It generates
2b7e4c1a-…and sends thePUTfrom Send the insight withstartedAt10:00. The response hasendedAt10:00 andoutcome: raised; the school's stage forWasteGamesisNOTIFY, so the insight shows in the student's session at once and subscribers receiveinsight.created. - Retry. The service's queue redelivers the same call. The response is the same insight, unchanged, and no new webhook is sent.
- End. At 10:04 the student closes the game. The service sends the same id with
endedAt10:04, as in Move the start and the end. The insight now lasts four minutes, and subscribers receiveinsight.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.
