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

# Reference

> Parameters, properties, methods, and callbacks for activity tracking

## Client

### `activity.current`

The currently active activity instance, or `null` if none is running.

### `activity.start(params)`

Creates and starts a new activity tracker. Heartbeats begin immediately unless `time: false` is passed.

<Warning>
  Only one activity can be active at a time. Calling `start()` while an activity is already
  running throws an error. End the current activity first.
</Warning>

<ResponseField name="id" type="string" required>
  Activity slug: a stable, URL-safe identifier for the learning object (e.g.
  `"fractions-with-like-denominators"`). Used to construct the canonical activity URL in Caliper
  events.
</ResponseField>

<ResponseField name="name" type="string" required>
  Human-readable display name (e.g. `"Fractions with Like Denominators"`). Sent as
  `object.activity.name` in Caliper events.
</ResponseField>

<ResponseField name="course" type="object" required>
  Course selector: must match a course in `timeback.config.json`. Either `{ subject, grade }` or `{ code }`.
</ResponseField>

<ResponseField name="runId" type="string">
  Unique identifier for correlating events across sessions. If omitted, the SDK generates a new
  UUID. Pass the same `runId` when resuming a [stateful
  activity](/beta/build-on-timeback/sdk/activity-tracking/stateful) to link heartbeats with the
  eventual completion event.
</ResponseField>

<ResponseField name="time" type="object | false">
  Time tracking configuration. All fields are optional: defaults work well for most apps.

  <Tip>
    Set to `false` to disable client-side time tracking entirely. When disabled, no heartbeats are
    sent, no visibility handlers are registered, and `end()` skips the final time flush. Use this when e.g. time is managed server-side.
  </Tip>

  <Expandable title="time options">
    <ResponseField name="flushIntervalMs" type="number" default="15000">
      Interval in milliseconds between automatic heartbeat flushes.
    </ResponseField>

    <ResponseField name="visibilityAware" type="boolean" default="true">
      Pause time tracking when the browser tab is not visible.
    </ResponseField>

    <ResponseField name="flushOnVisibilityHidden" type="boolean" default="true">
      Flush accumulated time immediately when the tab becomes hidden.
    </ResponseField>

    <ResponseField name="flushOnPageHide" type="boolean" default="true">
      Attempt a best-effort flush on page unload via `sendBeacon` or `fetch({ keepalive: true })`.
    </ResponseField>

    <ResponseField name="hiddenTimeoutMs" type="number | null" default="600000">
      Stop tracking after this many milliseconds of hidden time (default 10 minutes). Prevents counting abandoned-tab time. Set to `null` or `Infinity` to disable.
    </ResponseField>

    <ResponseField name="retryAttempts" type="number" default="0">
      Number of retry attempts for failed heartbeat sends. `0` means no retry (default). Retries use exponential backoff with delays from `retryDelaysMs`.
    </ResponseField>

    <ResponseField name="retryDelaysMs" type="number[]" default="[100, 300, 1000]">
      Delay schedule (in milliseconds) between retry attempts. Each index corresponds to the delay before that attempt. If more attempts than entries, the last value is reused.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="onError" type="(error, context) => void">
  Called when a time-spent flush or completion submission fails. When `retryAttempts` is configured, only fires after all retries are exhausted. Time-spent errors are non-fatal: the SDK continues tracking. Completion errors (from `end()`) are also surfaced here before being re-thrown.

  <Expandable title="callback signature">
    <ResponseField name="error" type="Error">
      The error that occurred.
    </ResponseField>

    <ResponseField name="context.type" type="'timeSpent' | 'completion'">
      Which operation failed. Aligns with Caliper event types:

      * `TimeSpentEvent`
      * `ActivityCompletedEvent`
    </ResponseField>

    <ResponseField name="context.activityId" type="string">
      The activity slug passed to `activity.start()`.
    </ResponseField>

    <ResponseField name="context.runId" type="string">
      The `runId` for this activity instance.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="onPause" type="() => void">
  Called when the activity is paused, either explicitly via `pause()` or automatically when the
  [hidden timeout](/beta/build-on-timeback/sdk/activity-tracking/intro#abandoned-tab-detection)
  fires.
</ResponseField>

<ResponseField name="onResume" type="() => void">
  Called when the activity resumes, either via `resume()` or when the user returns after a hidden
  timeout.
</ResponseField>

<ResponseField name="onFlush" type="(elapsedMs: number) => void">
  Called after each successful heartbeat flush with the active milliseconds reported in that
  window.
</ResponseField>

#### Examples

<CodeGroup>
  ```typescript Default theme={null}
  const activity = timeback.activity.start({
  	id: 'lesson-1',
  	name: 'Intro to Fractions',
  	course: { subject: 'Math', grade: 3 },
  })
  ```

  ```typescript Custom time options theme={null}
  const activity = timeback.activity.start({
  	id: 'lesson-1',
  	name: 'Intro to Fractions',
  	course: { subject: 'Math', grade: 3 },
  	time: {
  		flushIntervalMs: 30000, // 30 seconds
  		visibilityAware: false, // track even when tab is hidden
  	},
  })
  ```

  ```typescript With callbacks theme={null}
  const activity = timeback.activity.start({
  	id: 'lesson-1',
  	name: 'Intro to Fractions',
  	course: { subject: 'Math', grade: 3 },
  	onError: (err, ctx) => {
  		console.warn(`Activity ${ctx.type} failed:`, err.message)
  	},
  	onPause: () => showPausedOverlay(),
  	onResume: () => hidePausedOverlay(),
  	onFlush: ms => updateTimeDisplay(ms),
  })
  ```

  ```typescript With retry theme={null}
  const activity = timeback.activity.start({
  	id: 'lesson-1',
  	name: 'Intro to Fractions',
  	course: { subject: 'Math', grade: 3 },
  	time: {
  		retryAttempts: 3,
  		retryDelaysMs: [100, 300, 1000],
  	},
  	onError: (err, ctx) => {
  		// Only fires after all retries are exhausted
  		console.warn(`${ctx.type} failed after retries:`, err.message)
  	},
  })
  ```

  ```typescript Disabled (server manages time) theme={null}
  const activity = timeback.activity.start({
  	id: 'lesson-1',
  	name: 'Intro to Fractions',
  	course: { subject: 'Math', grade: 3 },
  	time: false,
  })

  // No heartbeats are sent — end() sends completion only
  await activity.end({ xpEarned: 100 })
  ```
</CodeGroup>

### Activity instance

The object returned by `activity.start()`.

#### Properties

<ResponseField name="id" type="string">
  The activity slug passed to `start()`.
</ResponseField>

<ResponseField name="startedAt" type="Date">
  When the activity was started.
</ResponseField>

<ResponseField name="isPaused" type="boolean">
  Whether the activity is currently paused.
</ResponseField>

<ResponseField name="isEnded" type="boolean">
  Whether `end()` has completed successfully.

  <hr />

  Once true, the activity is no longer active and a new one can be started.
</ResponseField>

<ResponseField name="runId" type="string">
  Unique identifier correlating heartbeats and completion events for this run.
</ResponseField>

<ResponseField name="totalActiveMs" type="number">
  Cumulative active time across all flushed heartbeat windows plus the current in-progress window.
</ResponseField>

<ResponseField name="elapsedMs" type="number">
  Active time for the current heartbeat window only. Resets to 0 after each flush.
</ResponseField>

#### Methods

<ResponseField name="pause()" type="">
  Flushes accumulated time, then stops heartbeats until `resume()` is called. Fires `onPause` if
  provided.
</ResponseField>

<ResponseField name="resume()" type="">
  Starts a fresh tracking window and restarts heartbeats. Fires `onResume` if provided.
</ResponseField>

<ResponseField name="flushTimeSpent()" type="Promise<void>">
  Manually flush accumulated time to the server. No-op when time tracking is disabled or the
  activity is paused. Serialized — only one flush can be in flight at a time.
</ResponseField>

```typescript theme={null}
activity.pause() // Flushes time, stops heartbeats
activity.resume() // Fresh window, restarts heartbeats
```

<Tip>
  `onPause` and `onResume` callbacks also fire for automatic state changes like [hidden
  timeouts](/beta/build-on-timeback/sdk/activity-tracking/intro#abandoned-tab-detection) — use
  them to keep your UI in sync without polling `isPaused`.
</Tip>

### `activity.end(data?)`

Ends the activity. Always flushes remaining time data. If completion data is provided, also sends an [`ActivityCompletedEvent`](/beta/build-on-timeback/reference/events#activitycompletedevent).

```typescript theme={null}
// Time-only flush (no completion recorded)
await activity.end()

// With completion
await activity.end({
	xpEarned: 80,
	totalQuestions: 10,
	correctQuestions: 8,
	pctComplete: 100,
})
```

<Note>
  If the network call fails, the activity remains usable so the caller can retry. `onError` fires with `{ type: 'completion' }` before the error is re-thrown.

  ```typescript theme={null}
  const activity = timeback.activity.start({
  	id: 'lesson-1',
  	name: 'Fractions',
  	course: { subject: 'Math', grade: 3 },
  	onError: (err, ctx) => {
  		if (ctx.type === 'completion') showRetryButton()
  	},
  })
  ```
</Note>

#### Completion data

<ResponseField name="xpEarned" type="number" required>
  XP earned for this activity. Must follow the [1 XP = 1 focused
  minute](/beta/about-timeback/concepts/xp-system) rule.
</ResponseField>

<ResponseField name="totalQuestions" type="number">
  Total questions in the activity. Must be paired with `correctQuestions`.
</ResponseField>

<ResponseField name="correctQuestions" type="number">
  Questions answered correctly. Must be paired with `totalQuestions`.
</ResponseField>

<ResponseField name="masteredUnits" type="number">
  Number of **new** units (lessons) the student mastered during this activity. This is an
  incremental count, not a cumulative total. The server sums these across submissions and divides
  by [`totalLessons`](/beta/build-on-timeback/reference/configuration#course-progress-config) to
  auto-compute `pctComplete` when it is omitted. See [Course
  progress](/beta/build-on-timeback/sdk/activity-tracking/course-progress) for details.
</ResponseField>

<ResponseField name="pctComplete" type="number">
  Course completion percentage (0--100). If omitted and `masteredUnits` is provided, the server
  [auto-computes this](/beta/build-on-timeback/sdk/activity-tracking/course-progress) from
  gradebook history. If provided, the server forwards it as-is.
</ResponseField>

<ResponseField name="time" type="object">
  Override the SDK's automatic time tracking with explicit values. Use this for advanced scenarios
  like offline sync or batch imports where the SDK wasn't tracking time in real-time.

  <Expandable title="time override fields">
    <ResponseField name="active" type="number">
      Total active milliseconds to report.
    </ResponseField>

    <ResponseField name="inactive" type="number">
      Total inactive (paused) milliseconds to report.
    </ResponseField>
  </Expandable>
</ResponseField>

<Note>
  **`totalQuestions` and `correctQuestions` must be provided together.**

  <hr />

  If you provide one, you must provide the other.
</Note>

***

## Server

### `timeback.activity.record(params)`

Records an activity completion from the backend. Sends an [`ActivityCompletedEvent`](/beta/build-on-timeback/reference/events#activitycompletedevent) to the Caliper API and runs the full pipeline including gradebook and [XP](/beta/about-timeback/concepts/xp-system).

See the [server adapter docs](/beta/build-on-timeback/sdk/server/nextjs) for setup.

<ResponseField name="user" type="object" required>
  Student identity. Provide `email` (required) and optionally `timebackId`. See
  [Identity](/beta/build-on-timeback/sdk/identity) for how users are resolved.
</ResponseField>

<ResponseField name="activity" type="object" required>
  Activity identity.

  <Expandable>
    <ResponseField name="id" type="string" required>
      Unique activity identifier
    </ResponseField>

    <ResponseField name="name" type="string" required>
      Human-readable activity name
    </ResponseField>

    <ResponseField name="course" type="object" required>
      Course reference — provide `code` (course code), or `subject` and `grade`
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="metrics" type="object" required>
  Completion metrics.

  <Expandable>
    <ResponseField name="xpEarned" type="number" required>
      XP earned for this activity. Must follow the [1 XP = 1 focused
      minute](/beta/about-timeback/concepts/xp-system) rule.
    </ResponseField>

    <ResponseField name="totalQuestions" type="number">
      Total questions in the activity
    </ResponseField>

    <ResponseField name="correctQuestions" type="number">
      Questions answered correctly
    </ResponseField>

    <ResponseField name="masteredUnits" type="number">
      Number of **new** units (lessons) mastered during this activity. This is incremental,
      not cumulative. See [Course
      progress](/beta/build-on-timeback/sdk/activity-tracking/course-progress).
    </ResponseField>

    <ResponseField name="pctComplete" type="number">
      Course completion percentage (0--100). Auto-computed from `masteredUnits` when omitted.
      See [Course progress](/beta/build-on-timeback/sdk/activity-tracking/course-progress).
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="time" type="object">
  Optional time data. Include when the backend tracks accumulated session time — for example, when
  the frontend uses [`time:
    	false`](/beta/build-on-timeback/sdk/activity-tracking/stateful#server-managed-time), or for
  offline sync and batch imports.

  <Expandable>
    <ResponseField name="startedAt" type="string">
      ISO 8601 timestamp of when the activity was first started
    </ResponseField>

    <ResponseField name="endedAt" type="string">
      ISO 8601 timestamp of when the activity was completed
    </ResponseField>

    <ResponseField name="activeMs" type="number">
      Total active milliseconds across all sessions
    </ResponseField>

    <ResponseField name="inactiveMs" type="number">
      Total inactive (paused) milliseconds across all sessions
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="runId" type="string">
  UUID correlating this completion with frontend heartbeats. Should match the `runId` persisted
  when the activity was started on the client.
</ResponseField>

<Note>
  **`totalQuestions` and `correctQuestions` must be provided together.**

  <hr />

  If you provide one, you must provide the other.
</Note>

#### Examples

<CodeGroup>
  ```typescript TypeScript theme={null}
  await timeback.activity.record({
  	user: { email: 'student@example.com' },
  	activity: {
  		id: 'lesson-1',
  		name: 'Fractions',
  		course: { code: 'MATH-3' },
  	},
  	metrics: {
  		totalQuestions: 20,
  		correctQuestions: 16,
  		xpEarned: 150,
  		masteredUnits: 2,
  		pctComplete: 100,
  	},
  	runId: savedProgress.runId,
  })
  ```

  ```python Python theme={null}
  await timeback.activity.record({
      "user": {"email": "student@example.com"},
      "activity": {
          "id": "lesson-1",
          "name": "Fractions",
          "course": {"code": "MATH-3"},
      },
      "metrics": {
          "total_questions": 20,
          "correct_questions": 16,
          "xp_earned": 150,
          "mastered_units": 2,
          "pct_complete": 100,
      },
      "run_id": saved_progress.run_id,
  })
  ```
</CodeGroup>

#### With time data

Only include `time` when the frontend uses [`time: false`](/beta/build-on-timeback/sdk/activity-tracking/reference#activity-start-params), meaning your server owns time tracking. Do **not** pass `time` here if the frontend is sending heartbeats (the default), or time will be double-counted.

<CodeGroup>
  ```typescript TypeScript theme={null}
  await timeback.activity.record({
  	user: { email: 'student@example.com' },
  	activity: {
  		id: 'lesson-1',
  		name: 'Fractions',
  		course: { code: 'MATH-3' },
  	},
  	metrics: {
  		totalQuestions: 20,
  		correctQuestions: 16,
  		xpEarned: 150,
  	},
  	time: {
  		activeMs: 1200000, // 20 minutes total active time
  		inactiveMs: 120000, // 2 minutes total paused time
  	},
  	runId: savedProgress.runId,
  })
  ```

  ```python Python theme={null}
  await timeback.activity.record({
      "user": {"email": "student@example.com"},
      "activity": {
          "id": "lesson-1",
          "name": "Fractions",
          "course": {"code": "MATH-3"},
      },
      "metrics": {
          "total_questions": 20,
          "correct_questions": 16,
          "xp_earned": 150,
      },
      "time": {
          "active_ms": 1200000,  # 20 minutes total active time
          "inactive_ms": 120000, # 2 minutes total paused time
      },
      "run_id": saved_progress.run_id,
  })
  ```
</CodeGroup>
