> ## 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.

# Caliper

> Configure the Caliper client for the event contract assigned to your integration

The Caliper client builds and sends learning events. Available routes, payload transformation, response fields, validation, and historical queries depend on the selected platform. Start with [Event Contracts](/beta/build-on-timeback/reference/event-contracts).

## Platform capabilities

| Client platform | Default submission path | Query, validation, and job methods                                |
| --------------- | ----------------------- | ----------------------------------------------------------------- |
| `BEYOND_AI`     | `/caliper/event`        | Legacy `validate`, `list`, `get`, `stream`, and job polling paths |
| `LEARNWITH_AI`  | `/caliper/v1p2`         | Send-only in this client; unsupported methods raise an error      |

The default platform is `BEYOND_AI`. `LEARNWITH_AI` applies a destination-specific event transformation before submission. Changing only the base URL does not prove that the selected paths or payload schema match another API. The Partner Events API at `/events/1.0` has a separate contract.

## Installation

<CodeGroup>
  ```bash TypeScript theme={null}
  npm install @timeback/caliper
  ```

  ```bash Python theme={null}
  pip install timeback-caliper
  ```
</CodeGroup>

## Send an activity

Run credentialed client code on a trusted backend. These examples explicitly select the legacy contract and supply synthetic identities. Replace them with assigned user, course, and app values.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { CaliperClient } from '@timeback/caliper'

  const client = new CaliperClient({
    platform: 'BEYOND_AI',
    env: 'staging',
    auth: {
      clientId: process.env.CALIPER_CLIENT_ID!,
      clientSecret: process.env.CALIPER_CLIENT_SECRET!,
    },
  })

  const result = await client.events.sendActivity('https://learning.example.com', {
    id: 'urn:uuid:437620ad-f9c9-4a9d-9232-37676bfcf28e',
    eventTime: '2026-09-11T15:00:00.000Z',
    actor: {
      id: 'https://api.example.com/ims/oneroster/rostering/v1p2/users/student-example',
      type: 'TimebackUser',
      email: 'learner@example.com',
    },
    object: {
      id: 'https://learning.example.com/activities/fractions',
      type: 'TimebackActivityContext',
      subject: 'Math',
      app: { name: 'Example Learning App' },
      course: {
        id: 'https://api.example.com/ims/oneroster/rostering/v1p2/courses/course-example',
        name: 'Fractions',
      },
      activity: { name: 'Adding fractions' },
      process: true,
    },
    metrics: [
      { type: 'totalQuestions', value: 5 },
      { type: 'correctQuestions', value: 4 },
      { type: 'xpEarned', value: 10 },
      { type: 'masteredUnits', value: 0 },
    ],
  })

  if (result.jobId && result.jobId !== '0') {
    const job = await client.jobs.waitForCompletion(result.jobId)
    console.log(job.state, job.returnValue)
  }
  ```

  ```python Python theme={null}
  import os
  from timeback_caliper import CaliperClient, ActivityCompletedInput

  client = CaliperClient(
      platform="BEYOND_AI",
      env="staging",
      client_id=os.environ["CALIPER_CLIENT_ID"],
      client_secret=os.environ["CALIPER_CLIENT_SECRET"],
  )

  # Run this code inside your async application.
  result = await client.events.send_activity(
      "https://learning.example.com",
      ActivityCompletedInput(
          id="urn:uuid:437620ad-f9c9-4a9d-9232-37676bfcf28e",
          event_time="2026-09-11T15:00:00.000Z",
          actor={
              "id": "https://api.example.com/ims/oneroster/rostering/v1p2/users/student-example",
              "type": "TimebackUser",
              "email": "learner@example.com",
          },
          object={
              "id": "https://learning.example.com/activities/fractions",
              "type": "TimebackActivityContext",
              "subject": "Math",
              "app": {"name": "Example Learning App"},
              "course": {
                  "id": "https://api.example.com/ims/oneroster/rostering/v1p2/courses/course-example",
                  "name": "Fractions",
              },
              "activity": {"name": "Adding fractions"},
              "process": True,
          },
          metrics=[
              {"type": "totalQuestions", "value": 5},
              {"type": "correctQuestions", "value": 4},
              {"type": "xpEarned", "value": 10},
              {"type": "masteredUnits", "value": 0},
          ],
      ),
  )

  if result.job_id and result.job_id != "0":
      job = await client.jobs.wait_for_completion(result.job_id)
      print(job.state, job.return_value)
  ```
</CodeGroup>

The numeric XP value is supplied by the producer. It is not calculated from elapsed time. See [Timeback activity and time events](/beta/build-on-timeback/reference/timeback-events) for metric fields and [SDK events](/beta/build-on-timeback/reference/events) for SDK-specific construction.

## Build once, retry the same event

`sendActivity` and `sendTimeSpent` create a fresh event ID and timestamp when you omit them. Calling a convenience method again without preserving those fields can create a distinct event.

For durable delivery, build the event once using `createActivityEvent` or `createTimeSpentEvent`, retain the event payload, and send it with `client.events.send(sensor, events)`. The Python factories are `create_activity_event` and `create_time_spent_event`. Preserve the original event and metric collection identities across transport retries.

Do not edit a submitted payload and reuse its event ID as a replacement operation. The legacy event store retains the first content for an external ID. Use your integration's correction procedure.

## Methods

| Operation                | TypeScript                            | Python                                     |
| ------------------------ | ------------------------------------- | ------------------------------------------ |
| Send event array         | `events.send(sensor, events)`         | `events.send(sensor_id, events)`           |
| Send envelope            | `events.sendEnvelope(envelope)`       | `events.send_envelope(envelope)`           |
| Activity helper          | `events.sendActivity(sensor, input)`  | `events.send_activity(sensor_id, input)`   |
| Time helper              | `events.sendTimeSpent(sensor, input)` | `events.send_time_spent(sensor_id, input)` |
| Validate without storage | `events.validate(envelope)`           | `events.validate(envelope)`                |
| List stored events       | `events.list(params)`                 | `events.list(...)`                         |
| Get external event ID    | `events.get(externalId)`              | `events.get(external_id)`                  |
| Iterate results          | `events.stream(params)`               | `events.stream(...)`                       |
| Inspect a job            | `jobs.getStatus(jobId)`               | `jobs.get_status(job_id)`                  |
| Poll a job               | `jobs.waitForCompletion(jobId)`       | `jobs.wait_for_completion(job_id)`         |

Validation, queries, and polling require a configured path for that operation. The send result's `jobId`/`job_id` can be absent. The batch API's compatibility value `"0"` is not a job to poll.

## Job monitoring

For a legacy job, polling stops when queue state is `completed`, and raises for `failed` or timeout. It does not inspect every semantic error in the worker result. Inspect `returnValue` in TypeScript or `return_value` in Python, including any error or partial-result information.

A completed queue job does not prove that all webhook subscribers or downstream analytics succeeded. Verify the expected persisted result separately.

## Historical queries

Legacy event listing supports limit, offset, sensor, actor ID, actor email, start date, and end date. For example:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const { events } = await client.events.list({
    limit: 20,
    sensor: 'https://learning.example.com',
    startDate: '2026-09-11T00:00:00.000Z',
  })
  ```

  ```python Python theme={null}
  result = await client.events.list(
      limit=20,
      sensor="https://learning.example.com",
      start_date="2026-09-11T00:00:00.000Z",
  )
  events = result.events
  ```
</CodeGroup>

The legacy query store is not a complete search of events sent through other ingestion systems. Check the event destination before treating an empty query as a missing send.

## Generic and question events

`events.send` accepts event objects, but the server still enforces its selected schema. Support for an event in one platform does not guarantee support in another, and schema acceptance does not itself award XP.

The client also provides question-seen, question-answered, and question-graded factories. Their destination-specific shapes should be checked against the assigned contract before use. Use the [Partner Event Reference](/beta/build-on-timeback/reference/partner-events) or [Platform Event Reference](/beta/build-on-timeback/reference/platform-events) for those APIs.

## Error handling

The current package does not export an `ApiError` class. Handle failures using the errors provided by your installed client version. A portable error boundary can inspect `error instanceof Error` for a message; avoid logging access tokens or raw student event payloads.

Correct validation and authorization failures before retrying. For ambiguous network or server failures, retain event identity and use bounded retries. Confirm the resulting activity through the destination's supported read APIs.
