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

### `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"`, `"ELA"`).
    </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

```typescript 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 // Total questions in the lesson
	metadata: object // 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>

### `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" required>
  Course reference for time tracking. 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"`, `"ELA"`).
    </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 lesson name.
</ResponseField>

<ResponseField name="forceNew" type="boolean" default="false">
  Start a fresh attempt instead of resuming an existing one. Without this, `lessons.start()` resumes the student's current in-progress 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` and `finalized` are
  exceptions: they update live as the student progresses.
</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. When `true`, scores are locked and the gradebook is updated.

  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.

  **Updates live**: set to `true` by `submit()` when the server signals completion (e.g., `powerpath-100` reaching score 100), and by `complete()` with the server's finalization 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), `session.finalized` is set to `true` and subsequent `next()` calls return `null`.

  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**: calls PowerPath's finalization endpoint to lock in the score, calculate accuracy, and trigger gradebook writes. For quiz types, this is where scoring actually happens. For `powerpath-100`, the score is already computed; this step confirms it.
  2. **Client**: calls `activity.end()` to flush the final time tracking heartbeat. If the flush fails, the result still contains valid scoring data. `timeTrackingSent` will be `false` and `error` will describe what went wrong.

  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()`:

```typescript 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>
  QTI XML parsing utilities are coming soon to the SDK. In the meantime, you'll need to parse the
  XML yourself to extract prompts, choices, and correct responses.
</Warning>

#### `LessonSubmitResult`

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

```typescript 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()`:

```typescript theme={null}
{
	score: number              // Final score
	totalQuestions: number     // Total questions
	correctQuestions: number   // Answered correctly
	accuracy: number           // Percentage (0-100)
	finalized: boolean         // Whether PowerPath finalized the attempt
	lessonType: LessonType     // The lesson type
	timeSpentSeconds: number   // Active learning time
	timeTrackingSent: boolean  // Whether the final time flush succeeded
	error?: string             // Error message if time flush failed
}
```

<Note>
  `timeTrackingSent` can be `false` even when the lesson completed successfully. The scoring data
  is always reliable; only the last \~15s of time data may be missing. See [Handle time tracking
  errors](/beta/build-on-timeback/sdk/managed-lessons/completion#handle-time-tracking-errors).
</Note>

### `lessons.attempts()`

Lists all attempts for a lesson.

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

#### Return type

```typescript theme={null}
{
	attempt: number // Attempt number (1, 2, 3, ...)
	score: number // Final score for this attempt
	finalized: boolean // Whether the attempt was completed
	totalQuestions: number
	correctQuestions: number
}
```

### `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`)**:

```typescript theme={null}
{
	lessonType: 'powerpath-100'
	seenQuestions: LessonAttemptQuestion[]
	score: number
	totalQuestions: number
	correctQuestions: number
}
```

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

```typescript theme={null}
{
	lessonType: 'quiz' | 'test-out' | ...
	questions: LessonAttemptQuestion[]
	score: number
	totalQuestions: number
	correctQuestions: number
}
```

#### `LessonAttemptQuestion`

```typescript theme={null}
{
	id: string // Question identifier
	correct: boolean // Whether the student answered correctly
	studentResponse: string // The student's submitted answer
	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'

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

```typescript 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. Omit for batch delivery.
</ResponseField>

#### Return type

```typescript 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

```typescript 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)`

Finalizes a lesson attempt — locks in the score and triggers gradebook writes. Unlike the client's `session.complete()`, this 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

```typescript 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>
