> ## 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 return types for Managed Lessons

## Client

<a id="lessons-list" />

### `lessons.list()`

Fetches available lessons from the courses configured in [`timeback.config.json`](/beta/build-on-timeback/reference/configuration).

<ResponseField name="course" type="ActivityCourseRef">
  Filter to a specific configured course. If omitted, returns lessons across all configured courses.

  <Expandable title="subject + grade selector">
    <ResponseField name="subject" type="TimebackSubject" required>
      Course subject (e.g. `"Math"`, `"Reading"`).
    </ResponseField>

    <ResponseField name="grade" type="TimebackGrade" required>
      Grade level (e.g. `5`).
    </ResponseField>
  </Expandable>

  <Expandable title="course code selector">
    <ResponseField name="code" type="string" required>
      Unique course code (e.g. `"CS-101"`). Use for grade-less apps.
    </ResponseField>
  </Expandable>
</ResponseField>

#### Return type

```text Response shape (not executable) theme={null}
{
    id: string // Unique lesson identifier
    name: string // Display name
    type: LessonType // 'powerpath-100' | 'quiz' | 'test-out' | ...
    courseId: string // Parent course ID
    questionCount?: number // May be absent
    metadata?: Record<string, unknown> // Additional lesson metadata
}
```

#### Examples

<CodeGroup>
  ```typescript All courses theme={null}
  const lessons = await timeback.lessons.list()
  ```

  ```typescript Filtered by course theme={null}
  const mathLessons = await timeback.lessons.list({
      course: { subject: 'Math', grade: 5 },
  })
  ```

  ```typescript By course code theme={null}
  const lessons = await timeback.lessons.list({
      course: { code: 'CS-101' },
  })
  ```
</CodeGroup>

<a id="lessons-start" />

### `lessons.start()`

Starts or resumes a lesson session. Returns a [`LessonSession`](#lesson-session) that manages the question loop and time tracking. An activity tracker starts automatically, so `TimeSpentEvent` heartbeats begin immediately.

<ResponseField name="lessonId" type="string" required>
  The lesson to start.
</ResponseField>

<ResponseField name="course" type="ActivityCourseRef">
  Course reference for time tracking. Required unless a client-level `defaultCourse` is configured. Must match a course in [`timeback.config.json`](/beta/build-on-timeback/reference/configuration). Use either subject + grade or a course code.

  <Expandable title="subject + grade selector">
    <ResponseField name="subject" type="TimebackSubject" required>
      Course subject (e.g. `"Math"`, `"Reading"`).
    </ResponseField>

    <ResponseField name="grade" type="TimebackGrade" required>
      Grade level (e.g. `5`).
    </ResponseField>
  </Expandable>

  <Expandable title="course code selector">
    <ResponseField name="code" type="string" required>
      Unique course code (e.g. `"CS-101"`). Use for grade-less apps.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="lessonType" type="LessonType">
  Determines how the SDK handles the question loop and completion. Pass `lesson.type` from the discovery result. The two primary types behave differently:

  * `powerpath-100`: adaptive (questions served one at a time, difficulty adjusts, session completes when score reaches 100)
  * `quiz`: static (all questions fetched in a batch, session completes via explicit finalization)

  Other quiz-like types (`test-out`, `placement`, `unit-test`) follow the same static pattern as `quiz`.
</ResponseField>

<ResponseField name="name" type="string">
  Display name for the activity tracker. Defaults to the server lesson type, then the lesson ID; pass `name: lesson.name` for the discovery title.
</ResponseField>

<ResponseField name="forceNew" type="boolean" default="false">
  Request a fresh attempt instead of reusing progress. The SDK catches errors from finalization/creation; inspect the returned attempt because this option does not guarantee that a new attempt was created. Without it, existing progress can include an already finalized attempt.

  When `true`, the behavior depends on the lesson type:

  * **Quiz-like types** (`quiz`, `test-out`, etc.): the current attempt is finalized first (scored with whatever was answered), then a new attempt is created
  * **`powerpath-100`**: PowerPath attempts cannot be finalized via the API, so a new attempt is created directly
</ResponseField>

#### Examples

<CodeGroup>
  ```typescript Default (resume existing) theme={null}
  const session = await timeback.lessons.start({
      lessonId: lesson.id,
      lessonType: lesson.type,
      course: { subject: 'Math', grade: 5 },
  })
  ```

  ```typescript Force new attempt theme={null}
  const session = await timeback.lessons.start({
      lessonId: lesson.id,
      lessonType: lesson.type,
      course: { subject: 'Math', grade: 5 },
      name: lesson.name,
      forceNew: true,
  })
  ```

  ```typescript By course code theme={null}
  const session = await timeback.lessons.start({
      lessonId: lesson.id,
      lessonType: lesson.type,
      course: { code: 'CS-101' },
  })
  ```
</CodeGroup>

### Lesson session

The object returned by `lessons.start()`. Manages the question loop, scoring state, and time tracking for a single lesson attempt.

#### Properties

<Note>
  Most properties are set once when the session is created. `score` updates after `submit()` and `complete()`; `finalized` updates only after `complete()`. These mutations do not automatically trigger framework rendering.
</Note>

<ResponseField name="lessonId" type="string">
  The lesson identifier passed to `lessons.start()`.
</ResponseField>

<ResponseField name="lessonType" type="LessonType">
  The delivery mode for this session. Resolved from the server response, falling back to the `lessonType` you passed to `lessons.start()`, then to `'quiz'`.

  Controls how the SDK fetches questions (`next()` hits the server each time for adaptive, fetches all at once for linear) and how completion works (adaptive auto-completes at score 100, linear requires explicit finalization).
</ResponseField>

<ResponseField name="attempt" type="number | undefined">
  The PowerPath attempt number (1, 2, 3, ...). `undefined` on first-ever access when no prior
  attempt data exists.
</ResponseField>

<ResponseField name="score" type="number">
  Cumulative score for the current attempt. Starts at the value returned by PowerPath (0 for new attempts, or the saved score when resuming).

  **Updates live** after each `submit()` call and again after `complete()` with the final score. For `powerpath-100`, this is a 0–100 mastery scale where reaching 100 means the lesson auto-completes. For quiz types, the score typically updates at finalization.
</ResponseField>

<ResponseField name="questionCount" type="number | undefined">
  Total questions in the lesson, as reported by PowerPath. For linear lessons (`quiz`, `test-out`,
  etc.), this is the fixed question count. For adaptive lessons (`powerpath-100`), this may be
  `undefined` since the total varies based on the student's performance.
</ResponseField>

<ResponseField name="seenQuestions" type="number">
  Number of questions the student had already seen when the session started. For `powerpath-100`,
  reflects how many questions were served before the session was resumed. For linear lesson types,
  always `0` (linear quizzes track answered questions differently via their local buffer).
</ResponseField>

<ResponseField name="finalized" type="boolean">
  Whether the lesson attempt has been finalized. This is the SDK/Pow­erPath progress flag, not an acknowledgement of all downstream gradebook or event delivery.

  Initial value depends on lesson type: for `powerpath-100`, always starts `false` (adaptive sessions are never pre-finalized). For quiz types, reflects whether the attempt was already completed when resuming.

  `submit()` updates the private question-loop completion state, not this public flag. `complete()` updates the flag from the server result.
</ResponseField>

#### Methods

<ResponseField name="next()" type="Promise<LessonQuestion | null>">
  Returns the next question, or `null` when there are no more questions.

  Behavior depends on lesson type:

  * **`powerpath-100`** (adaptive): each call makes a server round-trip. PowerPath selects the next question based on the student's performance so far. Returns `null` when PowerPath signals completion (score reached 100).
  * **`quiz` and other linear types**: the first call fetches **all** questions from the server in a single batch and buffers them locally. Subsequent calls return the next unanswered question from the buffer with no network request. Already-answered questions (from a resumed attempt) are skipped automatically.

  Returns `null` immediately if the session is already completed.
</ResponseField>

<ResponseField name="submit(input)" type="Promise<LessonSubmitResult>">
  Submits the student's answer to PowerPath and returns the scored result.

  After each call, `session.score` is updated with the server's response. If the server signals that the lesson is complete (e.g., `powerpath-100` reaching score 100), the private question-loop completion state is set and subsequent `next()` calls return `null`; public `session.finalized` is unchanged until `complete()`.

  For `powerpath-100`, `correct` reflects real-time scoring (the server evaluates immediately). For quiz types, `correct` is always `false` during the quiz. Evaluation happens at finalization.

  <Expandable title="parameters">
    <ResponseField name="question" type="string" required>
      The question ID being answered (from `LessonQuestion.id`).
    </ResponseField>

    <ResponseField name="response" type="string" required>
      The student's selected answer.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="complete()" type="Promise<LessonSessionCompleteResult>">
  Finalizes the lesson in two steps:

  1. **Server**: reads progress and, for unfinalized linear lessons, attempts finalization then re-reads progress. Errors in that finalization call are caught; the result can still have `finalized: false`. For adaptive lessons the SDK returns current progress with `finalized: true` without checking a mastery threshold.
  2. **Client**: calls `activity.end()` to flush the final time tracking heartbeat. If `end()` rejects, `timeTrackingSent` is false. Heartbeat network failures are usually swallowed inside the tracker, so true does not confirm server delivery.

  Updates `session.score` and `session.finalized` with the server's final values.
</ResponseField>

<ResponseField name="pause()" type="">
  Flushes any accumulated time to the server, then stops heartbeats until `resume()` is called.
  No-op if already paused or if the session has ended. Paused time is not counted as active
  learning time.
</ResponseField>

<ResponseField name="resume()" type="">
  Starts a fresh time tracking window and restarts heartbeats. No-op if not currently paused or if
  the session has ended.
</ResponseField>

#### `LessonQuestion`

The object returned by `session.next()`:

```text Response shape (not executable) theme={null}
{
    id: string             // Unique question identifier
    title?: string         // Optional display title
    difficulty?: string    // Optional difficulty level
    content?: {
        rawXml?: string     // QTI XML content for rendering
    }
}
```

The `content.rawXml` field contains [QTI](https://www.imsglobal.org/question/index.html) XML. Your app is responsible for parsing and rendering this.

<Warning>
  Your app supplies a renderer for supported interactions. The session submission API sends a single string in `responses.RESPONSE`; confirm that this matches your item before using this wrapper.
</Warning>

#### `LessonSubmitResult`

The object returned by `session.submit()`:

```text Response shape (not executable) theme={null}
{
    correct: boolean // Whether the answer was correct
    score: number // Updated cumulative score
    complete: boolean // Whether the lesson is now complete
}
```

#### `LessonSessionCompleteResult`

The object returned by `session.complete()`:

```text Response shape (not executable) theme={null}
{
    score: number              // Final score
    totalQuestions?: number    // May be absent
    correctQuestions?: number  // May be absent
    accuracy?: number          // May be absent
    finalized: boolean         // Whether PowerPath finalized the attempt
    lessonType: LessonType     // The lesson type
    timeSpentSeconds: number   // Active learning time
    timeTrackingSent: boolean  // Whether tracker cleanup resolved
    error?: string             // Error message if time flush failed
}
```

<Note>
  `timeTrackingSent` reports whether tracker cleanup threw. It is not delivery confirmation. Inspect `finalized` separately and reconcile missing analytics; data loss is not limited to the last heartbeat interval. See [Handle time tracking
  errors](/beta/build-on-timeback/sdk/managed-lessons/completion#handle-time-tracking-errors).
</Note>

<a id="lessons-attempts" />

### `lessons.attempts()`

Lists all attempts for a lesson.

<ResponseField name="lessonId" type="string" required>
  The lesson to list attempts for.
</ResponseField>

#### Return type

```text Response shape (not executable) theme={null}
{
    attempt?: number
    score: number
    startedAt?: string
    completedAt?: string
    scoreStatus?: string
}
```

<a id="lessons-attemptdetails" />

### `lessons.attemptDetails()`

Gets per-question data for a specific attempt. The response is a discriminated union based on lesson type.

<ResponseField name="lessonId" type="string" required>
  The lesson to review. Same `id` from the discovery result or the session's `lessonId`.
</ResponseField>

<ResponseField name="attempt" type="number" required>
  Which attempt to review (1, 2, 3, ...). Get available attempt numbers from
  [`lessons.attempts()`](#lessons-attempts).
</ResponseField>

#### Return type

**Adaptive lessons (`powerpath-100`)**:

```text Response shape (not executable) theme={null}
{
    lessonType: 'powerpath-100'
    seenQuestions: LessonAttemptQuestion[]
    score: number
    totalQuestions: number
    correctQuestions: number
}
```

**Linear lessons (`quiz`, `test-out`, etc.)**:

```text Response shape (not executable) theme={null}
{
    lessonType: 'quiz' | 'test-out' | 'placement' | 'unit-test'
    questions: LessonAttemptQuestion[]
    score: number
    totalQuestions: number
    correctQuestions: number
}
```

#### `LessonAttemptQuestion`

```text Response shape (not executable) theme={null}
{
    id: string // Question identifier
    correct?: boolean // May be absent
    response?: string | string[]
    responses?: Record<string, string | string[]>
    content?: {
        rawXml?: string // QTI XML for re-rendering
    }
}
```

### `getLessonAttemptQuestions()`

Helper that normalizes the discriminated `attemptDetails()` response into a flat `LessonAttemptQuestion[]`:

```typescript theme={null}
import { getLessonAttemptQuestions } from '@timeback/sdk/client'

const details = await timeback.lessons.attemptDetails({
    lessonId: lesson.id,
    attempt: 1,
})

const questions = getLessonAttemptQuestions(details)
// Always returns LessonAttemptQuestion[] regardless of lesson type
```

***

## Server

The server-side lessons namespace exposes the same operations without session management or automatic time tracking. Each operation is a standalone async function call — your backend controls the lifecycle directly.

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

<Note>
  The server namespace uses `lesson` and `student` identifiers directly, unlike the client API which uses `lessonId` and resolves the student from the authenticated session.
</Note>

### `timeback.lessons.list(input?)`

Lists lessons from the configured courses. Same behavior as the [client `lessons.list()`](#lessons-list).

<ResponseField name="course" type="ActivityCourseRef">
  Filter to a specific configured course. Accepts `{ subject, grade }` or `{ code }`. If omitted, returns lessons across all configured courses.
</ResponseField>

#### Examples

<CodeGroup>
  ```typescript TypeScript theme={null}
  const lessons = await timeback.lessons.list()
  const mathLessons = await timeback.lessons.list({ course: { subject: 'Math', grade: 5 } })
  ```

  ```python Python theme={null}
  lessons = await timeback.lessons.list()
  math_lessons = await timeback.lessons.list({"course": {"subject": "Math", "grade": 5}})
  ```
</CodeGroup>

### `timeback.lessons.start(input)`

Starts or resumes a lesson attempt. Unlike the client's `lessons.start()`, this returns a plain result object — no `LessonSession` or time tracking.

<ResponseField name="lesson" type="string" required>
  The lesson identifier (from `lessons.list()`).
</ResponseField>

<ResponseField name="student" type="string" required>
  The student's Timeback ID.
</ResponseField>

<ResponseField name="forceNew" type="boolean" default="false">
  Start a fresh attempt instead of resuming. Behaves the same as the [client `forceNew`](#lessons-start) option.
</ResponseField>

#### Return type

```text Response shape (not executable) theme={null}
{
    lessonId: string
    lessonType: LessonType
    attempt?: number
    score: number
    questionCount?: number
    seenQuestions: number
    finalized: boolean
}
```

#### Examples

<CodeGroup>
  ```typescript TypeScript theme={null}
  const result = await timeback.lessons.start({ lesson: 'lesson-1', student: 'tb_abc' })
  const fresh = await timeback.lessons.start({ lesson: 'lesson-1', student: 'tb_abc', forceNew: true })
  ```

  ```python Python theme={null}
  result = await timeback.lessons.start({"lesson": "lesson-1", "student": "tb_abc"})
  fresh = await timeback.lessons.start({"lesson": "lesson-1", "student": "tb_abc", "force_new": True})
  ```
</CodeGroup>

### `timeback.lessons.next(input)`

Returns the next question(s) for a lesson attempt. The return type depends on lesson type:

* **`powerpath-100`**: returns a single [`LessonQuestion`](#lessonquestion)
* **All other types**: returns a `LessonQuestionBatch` with all questions

<ResponseField name="lesson" type="string" required>
  The lesson identifier.
</ResponseField>

<ResponseField name="student" type="string" required>
  The student's Timeback ID.
</ResponseField>

<ResponseField name="lessonType" type="LessonType">
  Pass `"powerpath-100"` for adaptive single-question delivery. If omitted, the server inspects progress and routes adaptive lessons accordingly; a contradictory explicit type is rejected.
</ResponseField>

#### Return type

```text Response shape (not executable) theme={null}
// Single question (powerpath-100)
LessonQuestion

// Batch (quiz, test-out, etc.)
{
    questions: LessonQuestion[]
    answeredIds: string[]
    score: number
    finalized: boolean
    complete: boolean
}
```

#### Examples

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Quiz — returns all questions as a batch
  const batch = await timeback.lessons.next({ lesson: 'lesson-1', student: 'tb_abc' })

  // Adaptive — returns one question at a time
  const question = await timeback.lessons.next({
      lesson: 'lesson-1',
      student: 'tb_abc',
      lessonType: 'powerpath-100',
  })
  ```

  ```python Python theme={null}
  # Quiz — returns all questions as a batch
  batch = await timeback.lessons.next({"lesson": "lesson-1", "student": "tb_abc"})

  # Adaptive — returns one question at a time
  question = await timeback.lessons.next({
      "lesson": "lesson-1",
      "student": "tb_abc",
      "lesson_type": "powerpath-100",
  })
  ```
</CodeGroup>

### `timeback.lessons.submit(input)`

Submits a student's answer to a lesson question.

<ResponseField name="lesson" type="string" required>
  The lesson identifier.
</ResponseField>

<ResponseField name="student" type="string" required>
  The student's Timeback ID.
</ResponseField>

<ResponseField name="question" type="string" required>
  The question ID being answered (from `LessonQuestion.id`).
</ResponseField>

<ResponseField name="response" type="string" required>
  The student's selected answer.
</ResponseField>

#### Return type

```text Response shape (not executable) theme={null}
{
    correct: boolean
    score: number
    complete: boolean
    questionResult?: unknown
}
```

#### Examples

<CodeGroup>
  ```typescript TypeScript theme={null}
  const result = await timeback.lessons.submit({
      lesson: 'lesson-1',
      student: 'tb_abc',
      question: 'q-42',
      response: 'B',
  })
  ```

  ```python Python theme={null}
  result = await timeback.lessons.submit({
      "lesson": "lesson-1",
      "student": "tb_abc",
      "question": "q-42",
      "response": "B",
  })
  ```
</CodeGroup>

### `timeback.lessons.complete(input)`

Attempts linear finalization and returns progress, with the limitations described for `session.complete()` above. Unlike the client method, it does not flush time tracking.

<ResponseField name="lesson" type="string" required>
  The lesson identifier.
</ResponseField>

<ResponseField name="student" type="string" required>
  The student's Timeback ID.
</ResponseField>

#### Return type

```text Response shape (not executable) theme={null}
{
    lessonType: LessonType
    attempt?: number
    score: number
    totalQuestions?: number
    correctQuestions?: number
    accuracy?: number
    finalized: boolean
}
```

#### Examples

<CodeGroup>
  ```typescript TypeScript theme={null}
  const result = await timeback.lessons.complete({ lesson: 'lesson-1', student: 'tb_abc' })
  ```

  ```python Python theme={null}
  result = await timeback.lessons.complete({"lesson": "lesson-1", "student": "tb_abc"})
  ```
</CodeGroup>

### `timeback.lessons.attempts(input)`

Lists all attempts for a lesson/student pair. Same return type as the [client `lessons.attempts()`](#lessons-attempts).

<ResponseField name="lesson" type="string" required>
  The lesson identifier.
</ResponseField>

<ResponseField name="student" type="string" required>
  The student's Timeback ID.
</ResponseField>

#### Examples

<CodeGroup>
  ```typescript TypeScript theme={null}
  const attempts = await timeback.lessons.attempts({ lesson: 'lesson-1', student: 'tb_abc' })
  ```

  ```python Python theme={null}
  attempts = await timeback.lessons.attempts({"lesson": "lesson-1", "student": "tb_abc"})
  ```
</CodeGroup>

### `timeback.lessons.attemptDetails(input)`

Returns raw progress data for a specific attempt. See [client `lessons.attemptDetails()`](#lessons-attemptdetails) for the discriminated return shape.

<ResponseField name="lesson" type="string" required>
  The lesson identifier.
</ResponseField>

<ResponseField name="student" type="string" required>
  The student's Timeback ID.
</ResponseField>

<ResponseField name="attempt" type="number" required>
  Which attempt to review (1, 2, 3, ...).
</ResponseField>

#### Examples

<CodeGroup>
  ```typescript TypeScript theme={null}
  const details = await timeback.lessons.attemptDetails({
      lesson: 'lesson-1',
      student: 'tb_abc',
      attempt: 1,
  })
  ```

  ```python Python theme={null}
  details = await timeback.lessons.attempt_details({
      "lesson": "lesson-1",
      "student": "tb_abc",
      "attempt": 1,
  })
  ```
</CodeGroup>
