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

  1. Register your App and obtain OAuth credentials. See Level 0: Register Your App and Authentication.

  2. 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
  3. Request the permission for each action type you issue. Each type has one permission, granted to your client's tenant on urn:platform. For RedirectOnLaunch it is signals:issue_action:redirect_on_launch. Without it, issuing that type returns 403; 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, a PUT to 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 to OPEN. Different fields return 409.
  • type, version and targetUserId never change under an id. Sending different ones returns 409.
  • An id another client issued, or the id of an action TimeBack's rules raised, returns 403.
  • Every PUT is 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 returns 403, 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 PUT with "state": "CANCELLED". While the action is OPEN, the fields you send replace the stored ones as usual. After that, send the stored fields unchanged: different fields return 409.
  • 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 of OPEN, SEEN, COMPLETED, SKIPPED, CANCELLED.
  • inWindow=true: only actions inside their window, with availableFrom not 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
  • occurredAt is 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 SKIPPED back to OPEN, returns 409.
  • 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.

  1. Issue. The coach's client generates 6f1c2a9e-… and sends the RedirectOnLaunch from Send the action, expiring at the end of the next day. The response is OPEN.
  2. 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 shows state: SEEN and seenAt.
  3. Finish. The student opens math: the app reports COMPLETED. Or the student dismisses the offer and continues to reading: the app reports SKIPPED. Either way the coach's read shows resolvedAt, and the action leaves the student's open list.
  4. If plans change first. Before the student sees it, the coach cancels it with PUT /signals/1.0/actions/6f1c2a9e-…/state and {"state":"CANCELLED"}. The app no longer receives it.
  5. If nothing happens. The student never launches the reading app before expiresAt. The action stays OPEN, the coach's read shows expired: 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": "..." }.