> ## Documentation Index
> Fetch the complete documentation index at: https://docs.timeback.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Timeback Activity and Time Events

> The TimebackProfile contract for direct Caliper integrations

This page applies to integrations assigned the direct Caliper metrics contract. Send an envelope to `POST /caliper/event` on your Caliper base URL. These event types are not accepted by the Partner Events API at `/events/1.0`.

## Envelope

| Field         | Requirement                                                       |
| ------------- | ----------------------------------------------------------------- |
| `sensor`      | URL identifying your registered event producer                    |
| `sendTime`    | UTC ISO 8601 send timestamp                                       |
| `dataVersion` | Exactly `http://purl.imsglobal.org/ctx/caliper/v1p2`              |
| `data`        | Nonempty array of events; use a different event ID for each event |

Each event requires `id`, `type`, `profile`, `actor`, `action`, `object`, and `eventTime`. Use `profile: "TimebackProfile"` and an event ID in `urn:uuid:` form. Use the standard Caliper `@context` value; the schema supplies it when absent.

## User and activity context

| Object            | Required fields                                               | Optional fields                 |
| ----------------- | ------------------------------------------------------------- | ------------------------------- |
| `actor`           | `id` URL, `type: "TimebackUser"`, valid `email`               | `name`, `role`, `extensions`    |
| `object`          | `id` URL, `type: "TimebackActivityContext"`, `subject`, `app` | `course`, `activity`, `process` |
| `object.app`      | `name`                                                        | `id` URL, `extensions`          |
| `object.course`   | `name`, when the course object is supplied                    | `id` URL, `extensions`          |
| `object.activity` | `name`, when the activity object is supplied                  | `id` URL, `extensions`          |

Allowed roles are `student`, `teacher`, `admin`, and `guide`. Allowed subjects are `Reading`, `Language`, `Vocabulary`, `Social Studies`, `Writing`, `Handwriting`, `Science`, `FastMath`, `Math`, `None`, and `Other`.

Use the student's Timeback user URL and the course URL assigned to your integration. The schema permits a course without an ID, but downstream course resolution can require it. For activity events destined for the TimebackProfile analytics worker, set `object.process` to `true`. It is a processing instruction, not a completion percentage. Confirm your assigned consumer before applying it to another platform.

## ActivityEvent

Set `type` to `ActivityEvent` and `action` to `Completed`. `generated` requires an ID URL, type `TimebackActivityMetricsCollection`, and an `items` array.

| Metric type        | Meaning                                                 |
| ------------------ | ------------------------------------------------------- |
| `xpEarned`         | XP supplied by the producer                             |
| `totalQuestions`   | Number of questions represented by the activity         |
| `correctQuestions` | Number of correct questions represented by the activity |
| `masteredUnits`    | Number of mastered units represented by the activity    |

Each item contains a numeric `value`; item and collection `extensions` are optional. The schema does not infer question counts, compute correctness, or verify a mastery decision. Supply values that match the agreed measurement rule for your app.

`action: "Completed"` describes an activity. It does not mean that the course is complete.

## TimeSpentEvent

Set `type` to `TimeSpentEvent` and `action` to `SpentTime`. `generated` requires an ID URL, type `TimebackTimeSpentMetricsCollection`, and an `items` array.

| Field                             | Meaning                                                     |
| --------------------------------- | ----------------------------------------------------------- |
| `items[].type`                    | `active`, `inactive`, `waste`, `unknown`, or `anti-pattern` |
| `items[].value`                   | Duration in seconds, at most 86,400 per item                |
| `items[].subType`                 | Optional detail about the measured activity                 |
| `items[].startDate` and `endDate` | Optional UTC ISO 8601 timestamps                            |
| `items[].extensions`              | Optional metadata                                           |

Send nonnegative durations that match the measured interval. The current schema enforces the upper duration bound; it does not enforce a lower bound, timestamp ordering, or equality between duration and timestamp difference. Validate these conditions in your producer.

Time measurement is separate from XP measurement. Replaying an interval under a new event identity can add time again in downstream processing. Preserve the event identity for a transport retry and retain your own record of sent intervals.

## Example envelope

This synthetic example records an activity and five minutes of active time. Replace its IDs, addresses, sensor, and timestamps with the values assigned to your integration.

```json theme={null}
{
  "sensor": "https://learning.example.com",
  "sendTime": "2026-09-11T15:00:00.000Z",
  "dataVersion": "http://purl.imsglobal.org/ctx/caliper/v1p2",
  "data": [
    {
      "@context": "http://purl.imsglobal.org/ctx/caliper/v1p2",
      "id": "urn:uuid:437620ad-f9c9-4a9d-9232-37676bfcf28e",
      "eventTime": "2026-09-11T15:00:00.000Z",
      "profile": "TimebackProfile",
      "actor": {
        "id": "https://api.example.com/ims/oneroster/rostering/v1p2/users/student-example",
        "type": "TimebackUser",
        "email": "learner@example.com",
        "role": "student"
      },
      "object": {
        "id": "https://learning.example.com/activities/activity-example",
        "type": "TimebackActivityContext",
        "subject": "Math",
        "process": true,
        "app": {
          "name": "Example Learning App"
        },
        "course": {
          "id": "https://api.example.com/ims/oneroster/rostering/v1p2/courses/course-example",
          "name": "Fractions"
        },
        "activity": {
          "name": "Adding fractions"
        }
      },
      "type": "ActivityEvent",
      "action": "Completed",
      "generated": {
        "id": "https://learning.example.com/metrics/activity-example",
        "type": "TimebackActivityMetricsCollection",
        "items": [
          {
            "type": "xpEarned",
            "value": 10
          },
          {
            "type": "totalQuestions",
            "value": 5
          },
          {
            "type": "correctQuestions",
            "value": 4
          },
          {
            "type": "masteredUnits",
            "value": 0
          }
        ]
      }
    },
    {
      "@context": "http://purl.imsglobal.org/ctx/caliper/v1p2",
      "id": "urn:uuid:9c4a6261-26ad-4a3c-9329-466d41ebbc4d",
      "eventTime": "2026-09-11T15:00:00.000Z",
      "profile": "TimebackProfile",
      "actor": {
        "id": "https://api.example.com/ims/oneroster/rostering/v1p2/users/student-example",
        "type": "TimebackUser",
        "email": "learner@example.com",
        "role": "student"
      },
      "object": {
        "id": "https://learning.example.com/activities/activity-example",
        "type": "TimebackActivityContext",
        "subject": "Math",
        "process": true,
        "app": {
          "name": "Example Learning App"
        },
        "course": {
          "id": "https://api.example.com/ims/oneroster/rostering/v1p2/courses/course-example",
          "name": "Fractions"
        },
        "activity": {
          "name": "Adding fractions"
        }
      },
      "type": "TimeSpentEvent",
      "action": "SpentTime",
      "generated": {
        "id": "https://learning.example.com/metrics/time-example",
        "type": "TimebackTimeSpentMetricsCollection",
        "items": [
          {
            "type": "active",
            "value": 300,
            "startDate": "2026-09-11T14:55:00.000Z",
            "endDate": "2026-09-11T15:00:00.000Z"
          }
        ]
      }
    }
  ]
}
```

## Validate and confirm

Use `POST /caliper/event/validate` with the same envelope to check its schema without storing it. On success, the validator returns `200` with `status: "success"` and `message: "Request payload is valid"`.

Ingestion returns `200` after validation and queuing. Retain its `jobId`. For the Caliper service, `GET /jobs/{jobId}/status` exposes queue state and the worker's `returnValue`. Check result status and errors as well as the queue state. A completed queue job does not prove that every downstream subscriber succeeded.

Do not replace an event payload by resending a changed payload under the same event ID. The Caliper service keeps the first stored event content for that ID. Corrections need the correction procedure agreed for the integration.
