Signals Actions
Ask TimeBack to act on a student, for example "when this student next opens app A, offer app B first", and follow what happened.
Note: This guide covers the integration flow. See the API Reference for complete endpoint documentation, all parameters, and response schemas.
What an action is
An action is one thing TimeBack should make happen for one student. Some are raised by TimeBack's own rules, such as an integrity warning. Others are issued by a trusted app through this API, such as a coach redirect. Issued actions are what this guide covers.
| Field | Meaning |
|---|---|
type, version |
What kind of action it is, for example RedirectOnLaunch version 1. Together they select the schema attributes must match |
targetUserId |
The student the action is for. Required |
tbSessionId |
A session of that student the action belongs to. Optional |
organizationId |
The organization the action is issued under. Defaults to the student's primary organization |
message |
What the student is told. Optional |
attributes |
Fields specific to the type |
availableFrom, expiresAt |
The delivery window. Both optional: no availableFrom means deliverable at once, no expiresAt means it never expires |
initiatorUserId |
The person who started the action, such as a guide, when one is recorded. Read-only |
state |
Where the action is in its lifecycle (below) |
seenAt, resolvedAt |
When the student's app displayed it, and when it ended |
The issuer is the API client whose token created the action. Only that client can change it, cancel it, or read it back.
States
| State | Meaning | Timestamp set |
|---|---|---|
OPEN |
Issued, not yet shown to the student | none |
SEEN |
The student's app displayed it | seenAt |
COMPLETED |
The student did what the action asked | resolvedAt |
SKIPPED |
The student declined | resolvedAt |
CANCELLED |
The issuer withdrew it before it ended | resolvedAt |
COMPLETED, SKIPPED and CANCELLED are final. An action cancelled before the student saw it keeps seenAt empty.
An action stored on a session that TimeBack later revoked is hidden: reads and state moves treat it as not found.
Expiry is not a state. An action is expired when expiresAt has passed and it is still OPEN or SEEN. Reads return this as the expired flag, and the student's app stops receiving expired actions.
Getting access
-
Register your App and obtain OAuth credentials. See Level 0: Register Your App and Authentication.
-
Request the Signals scopes for your M2M client:
- Issue and cancel:
https://timeback-platform.trilogy.com/signals/scope/actions.write - Read back:
https://timeback-platform.trilogy.com/signals/scope/insights.readonly
- Issue and cancel:
-
Request the permission for each action type you issue. Each type has one permission, granted to your client's tenant on
urn:platform. ForRedirectOnLaunchit issignals:issue_action:redirect_on_launch. Without it, issuing that type returns403; once it is granted, the same request succeeds with no change on your side.
Ask for the scopes 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 on the issuer calls with 403.
Endpoints
| Method | Path | Scope | Caller | Purpose |
|---|---|---|---|---|
PUT |
/signals/1.0/actions/{actionId} |
actions.write |
Issuer | Issue an action, update it, or cancel it |
GET |
/signals/1.0/actions/{actionId} |
insights.readonly |
Issuer | Read one action you issued |
GET |
/signals/1.0/actions |
insights.readonly |
Issuer | List the actions you issued |
GET |
/signals/1.0/users/{userId}/actions |
insights.readonly |
The student's app, or an internal client | List a student's actions, filtered by state, window and creation time |
PUT |
/signals/1.0/actions/{actionId}/state |
events.write or actions.write |
The student's app, or the issuer to cancel | Move an action to its next state |
events.write is https://purl.imsglobal.org/spec/caliper/v1p2/scope/events.write.
Issue an action
1. Generate the action id
You choose the id: a UUID you generate and keep. Sending the same id again lands on the same action, so a request that timed out is safe to repeat.
2. Send the action
const actionId = crypto.randomUUID();
const response = await fetch(`https://platform.timeback.com/signals/1.0/actions/${actionId}`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'RedirectOnLaunch',
version: '1',
targetUserId: studentId,
message: 'Finish your math lesson first.',
expiresAt: '2026-10-21T23:59:59.000Z',
attributes: {
triggerApplicationId: readingAppId,
targetApplicationId: mathAppId,
targetLinkUri: 'https://math.example.com/launch',
},
}),
});
| Field | Type | Required | Description |
|---|---|---|---|
type |
string | Yes | The action type, for example RedirectOnLaunch |
version |
string | Yes | The type version, for example 1 |
targetUserId |
uuid | Yes | The student |
attributes |
object | Yes | The type's fields, below |
message |
string | No | What the student is told. 1 to 5000 characters |
tbSessionId |
string | No | A session of the same student |
organizationId |
uuid | No | Defaults to the student's primary organization. Required when the student has none |
availableFrom |
date-time | No | Not delivered before this instant |
expiresAt |
date-time | No | Not delivered after this instant. Must be after availableFrom |
state |
string | No | Only CANCELLED, to cancel (below) |
RedirectOnLaunch version 1
When the student next launches the trigger app, their TimeBack app offers the target app first.
| Attribute | Type | Description |
|---|---|---|
triggerApplicationId |
uuid | The app whose launch triggers the redirect |
targetApplicationId |
uuid | The app to offer instead. Must differ from the trigger |
targetLinkUri |
url | Where the target app is opened |
Both application ids must be existing applications; GET /applications/1.0 lists them. The attributes take exactly these three fields, and the body takes only the fields in the table above: anything else returns 400. Date-times are ISO 8601 with Z or an offset.
A 200 returns the action as stored:
{
"id": "6f1c2a9e-4b7d-4e3a-9c21-8d5f0b7a3e14",
"type": "RedirectOnLaunch",
"version": "1",
"targetUserId": "a1b2c3d4-0000-4000-8000-000000000002",
"organizationId": "b2c3d4e5-0000-4000-8000-000000000003",
"tbSessionId": null,
"message": "Finish your math lesson first.",
"availableFrom": null,
"expiresAt": "2026-10-21T23:59:59.000Z",
"attributes": {
"triggerApplicationId": "c3d4e5f6-0000-4000-8000-000000000004",
"targetApplicationId": "d4e5f6a7-0000-4000-8000-000000000005",
"targetLinkUri": "https://math.example.com/launch"
},
"state": "OPEN",
"seenAt": null,
"resolvedAt": null,
"dateCreated": "2026-10-20T15:00:00.000Z"
}
Retries and updates
- While
OPEN, aPUTto the same id replaces the action's fields with the ones you send. Send the whole action every time: an optional field you leave out is cleared. - After
OPEN, the fields are fixed. Repeating the stored fields returns the action unchanged, and never moves it back toOPEN. Different fields return409. type,versionandtargetUserIdnever change under an id. Sending different ones returns409.- An id another client issued, or the id of an action TimeBack's rules raised, returns
403. - Every
PUTis checked in full before it reaches the stored action: the body, the student and applications, and your permission for the type. A retry after the permission is withdrawn returns403, not the stored action.
Cancel an action
Withdraw an action that is still OPEN or SEEN in either of two ways. Both set state to CANCELLED and stamp resolvedAt.
- Send the issuing
PUTwith"state": "CANCELLED". While the action isOPEN, the fields you send replace the stored ones as usual. After that, send the stored fields unchanged: different fields return409. - Or call the state endpoint, which needs no fields and lets you say when it happened:
await fetch(`https://platform.timeback.com/signals/1.0/actions/${actionId}/state`, {
method: 'PUT',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ state: 'CANCELLED', occurredAt: '2026-10-20T16:30:00.000Z' }),
});
Cancelling an action that is already CANCELLED succeeds and changes nothing, on the PUT as long as the fields match. A COMPLETED or SKIPPED action cannot be cancelled: 409.
Delivery
The student's TimeBack app delivers issued actions. It calls these two endpoints with a token acting for that student; your client does not call them for the student.
The app reads open actions
The app reads the actions it should deliver with:
GET /signals/1.0/users/{userId}/actions?state=OPEN,SEEN&inWindow=true
No session is needed, so the app can read them before it launches anything.
With no filters, the endpoint returns every action for the student in any state, including actions not yet available and expired ones, each with its expired flag. Actions on a revoked session are never returned. The filters narrow it:
state: comma-separated, any ofOPEN,SEEN,COMPLETED,SKIPPED,CANCELLED.inWindow=true: only actions inside their window, withavailableFromnot in the future and not expired.createdAfter/createdBefore: inclusive bounds on when the platform received the action, as on the issuer list.type: one action type.
limit (at most 100) and offset page the list. A token acting for a student reads only that student's actions. TimeBack's internal clients read any student. Other client tokens are refused with 403.
The app reports progress
The app moves the action with PUT /signals/1.0/actions/{actionId}/state, body { "state": "...", "occurredAt": "..." }:
| Move | Made by | Sets |
|---|---|---|
OPEN → SEEN |
The student's app, when it shows the action | seenAt |
SEEN → COMPLETED or SKIPPED |
The student's app, when the student acts | resolvedAt |
OPEN or SEEN → CANCELLED |
The issuer only | resolvedAt |
occurredAtis optional; without it the server time is used.- Asking for the state the action already has succeeds and changes nothing.
- Any other move, for example
SKIPPEDback toOPEN, returns409. - A caller that may not make the move returns
403: the student's app cannot cancel, and the issuer cannot mark an action seen, completed or skipped.
A 200 returns actionId, state, seenAt, resolvedAt, and notificationRevision, a counter the TimeBack app uses to refresh what it shows the student.
Follow up on your actions
Read one action
const response = await fetch(`https://platform.timeback.com/signals/1.0/actions/${actionId}`, {
headers: { Authorization: `Bearer ${accessToken}` },
});
const action = await response.json();
{
"actionId": "6f1c2a9e-4b7d-4e3a-9c21-8d5f0b7a3e14",
"type": "RedirectOnLaunch",
"version": "1",
"attributes": {
"triggerApplicationId": "c3d4e5f6-0000-4000-8000-000000000004",
"targetApplicationId": "d4e5f6a7-0000-4000-8000-000000000005",
"targetLinkUri": "https://math.example.com/launch"
},
"message": "Finish your math lesson first.",
"state": "SEEN",
"expired": false,
"availableFrom": null,
"expiresAt": "2026-10-21T23:59:59.000Z",
"seenAt": "2026-10-21T08:02:11.000Z",
"resolvedAt": null,
"startedAt": null,
"endedAt": null,
"targetUserId": "a1b2c3d4-0000-4000-8000-000000000002",
"tbSessionId": null,
"organizationId": "b2c3d4e5-0000-4000-8000-000000000003",
"initiatorUserId": null
}
The reads name the id actionId; the issuing PUT returns it as id. startedAt and endedAt are only set on actions TimeBack's rules raised. An action another client issued returns 403; an unknown id returns 404.
List your actions
GET /signals/1.0/actions returns { "actions": [...], "offset", "limit", "total" }: every action your client issued, in every state, oldest first. Narrow it with:
| Parameter | Description |
|---|---|
targetUserId |
One student |
type |
One action type |
state |
One stored state. To find expired actions, read expired |
createdAfter |
Received at or after this instant |
createdBefore |
Received at or before this instant |
limit |
Page size, 1 to 100. Default 20 |
offset |
Rows to skip. Default 0 |
You never see another client's actions here.
Worked example: a coach redirect
A coach sees that a student opens a reading app every morning before finishing math, and wants the next launch to offer math first.
- Issue. The coach's client generates
6f1c2a9e-…and sends theRedirectOnLaunchfrom Send the action, expiring at the end of the next day. The response isOPEN. - Deliver. The next morning the student's TimeBack app reads the student's open actions, finds the redirect, and when the student launches the reading app it shows "Finish your math lesson first." with math offered. It reports
SEEN. The coach's read now showsstate: SEENandseenAt. - Finish. The student opens math: the app reports
COMPLETED. Or the student dismisses the offer and continues to reading: the app reportsSKIPPED. Either way the coach's read showsresolvedAt, and the action leaves the student's open list. - If plans change first. Before the student sees it, the coach cancels it with
PUT /signals/1.0/actions/6f1c2a9e-…/stateand{"state":"CANCELLED"}. The app no longer receives it. - If nothing happens. The student never launches the reading app before
expiresAt. The action staysOPEN, the coach's read showsexpired: true, and the app no longer receives it.
Errors
| Status | When |
|---|---|
400 |
The body or query fails validation: an unknown field, an unknown type/version pair, attributes that do not match its schema, an unknown student, application or organization, a session of another student, or expiresAt not after availableFrom |
401 |
The token is missing, expired, or lacks the scope the endpoint needs |
403 |
Your tenant lacks the permission for the type, the token is a person's or delegated to a person, another client issued the id, or the caller may not make the requested state move |
404 |
On a read or a state move: the action does not exist |
409 |
A change not allowed in the action's current state: changing type, version or targetUserId, changing fields after OPEN, or a state move outside the table above |
Error bodies carry error and message. A validation 400 also lists each failing input in fields, as { "field": "attributes.targetLinkUri", "message": "..." }.
