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

# QTI

> QTI API client for assessments and question items

## Overview

The `@timeback/qti` package provides a client for the QTI (Question and Test Interoperability) API, enabling:

* **Assessment Items**: Query and manage question items
* **Assessment Tests**: Query tests with nested structure
* **Stimuli**: Query shared content for questions
* **Lessons**: Submit and retrieve lesson feedback
* **Validation**: Validate QTI XML content

## Installation

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

  ```bash pnpm theme={null}
  pnpm add @timeback/qti
  ```

  ```bash yarn theme={null}
  yarn add @timeback/qti
  ```

  ```bash bun theme={null}
  bun add @timeback/qti
  ```

  ```bash pip theme={null}
  pip install timeback-qti
  ```

  ```bash uv theme={null}
  uv add timeback-qti
  ```
</CodeGroup>

## Quick Start

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

  const client = new QtiClient({
  	env: 'staging',
  	auth: {
  		clientId: process.env.QTI_CLIENT_ID!,
  		clientSecret: process.env.QTI_CLIENT_SECRET!,
  	},
  })

  const { items, total, page, pages } = await client.assessmentItems.list()
  const item = await client.assessmentItems.get('item-123')
  ```

  ```python Python theme={null}
  from timeback_qti import QtiClient

  client = QtiClient(
      env="staging",
      client_id=os.environ["QTI_CLIENT_ID"],
      client_secret=os.environ["QTI_CLIENT_SECRET"],
  )

  items = await client.assessment_items.list()
  item = await client.assessment_items.get("item-123")
  ```
</CodeGroup>

## Assessment Items

### List & Get Items

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.assessmentItems.list()
  client.assessmentItems.list({ page: 2, limit: 50, sort: 'title', order: 'asc' })
  client.assessmentItems.get(itemId)
  client.assessmentItems.stream()
  client.assessmentItems.stream({ max: 500 }).toArray()
  ```

  ```python Python theme={null}
  await client.assessment_items.list()
  await client.assessment_items.list({"page": 2, "limit": 50, "sort": "title", "order": "asc"})
  await client.assessment_items.get(item_id)
  async for item in client.assessment_items.stream():
      ...
  ```
</CodeGroup>

| Method       | Returns                         | Description                   |
| ------------ | ------------------------------- | ----------------------------- |
| `list()`     | `{ items, total, page, pages }` | List with pagination metadata |
| `get()`      | `AssessmentItem`                | Get item by ID                |
| `stream()`   | `AsyncIterable<Item>`           | Memory-efficient streaming    |
| `.toArray()` | `Item[]`                        | Collect streamed items        |

### Create, Update, Upsert & Delete

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.assessmentItems.create({ identifier, title, ...content })
  client.assessmentItems.update(itemId, { title, ...content })
  client.assessmentItems.upsert(itemId, { title, ...content })
  client.assessmentItems.delete(itemId)
  client.assessmentItems.processResponse(itemId, { response })
  ```

  ```python Python theme={null}
  await client.assessment_items.create({"identifier": identifier, "title": title, **content})
  await client.assessment_items.update(item_id, {"title": title, **content})
  await client.assessment_items.upsert(item_id, {"title": title, **content})
  await client.assessment_items.delete(item_id)
  await client.assessment_items.process_response(item_id, {"response": response})
  ```
</CodeGroup>

## Assessment Tests

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.assessmentTests.list()
  client.assessmentTests.get(testId)
  client.assessmentTests.stream()
  client.assessmentTests.create({ identifier, title, ...content })
  client.assessmentTests.update(testId, { title, ...content })
  client.assessmentTests.upsert(testId, { title, ...content })
  client.assessmentTests.delete(testId)
  client.assessmentTests.updateMetadata(testId, metadata)
  client.assessmentTests.getQuestions(testId)
  ```

  ```python Python theme={null}
  await client.assessment_tests.list()
  await client.assessment_tests.get(test_id)
  async for test in client.assessment_tests.stream():
      ...
  await client.assessment_tests.create({"identifier": identifier, "title": title, **content})
  await client.assessment_tests.update(test_id, {"title": title, **content})
  await client.assessment_tests.upsert(test_id, {"title": title, **content})
  await client.assessment_tests.delete(test_id)
  await client.assessment_tests.update_metadata(test_id, metadata)
  await client.assessment_tests.get_questions(test_id)
  ```
</CodeGroup>

### Test Parts

Assessment tests contain test parts, which contain sections:

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.assessmentTests.testParts(testId).list()
  client.assessmentTests.testParts(testId).get(testPartId)
  client.assessmentTests.testParts(testId).create({ identifier, ...content })
  client.assessmentTests.testParts(testId).update(testPartId, { ...content })
  client.assessmentTests.testParts(testId).delete(testPartId)
  ```

  ```python Python theme={null}
  await client.assessment_tests.test_parts(test_id).list()
  await client.assessment_tests.test_parts(test_id).get(test_part_id)
  await client.assessment_tests.test_parts(test_id).create({"identifier": identifier, **content})
  await client.assessment_tests.test_parts(test_id).update(test_part_id, {**content})
  await client.assessment_tests.test_parts(test_id).delete(test_part_id)
  ```
</CodeGroup>

### Sections

Sections live within a test part:

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.assessmentTests.testParts(testId).sections(testPartId).list()
  client.assessmentTests.testParts(testId).sections(testPartId).get(sectionId)
  client.assessmentTests.testParts(testId).sections(testPartId).create({ identifier, ...content })
  client.assessmentTests.testParts(testId).sections(testPartId).update(sectionId, { ...content })
  client.assessmentTests.testParts(testId).sections(testPartId).delete(sectionId)
  ```

  ```python Python theme={null}
  await client.assessment_tests.test_parts(test_id).sections(test_part_id).list()
  await client.assessment_tests.test_parts(test_id).sections(test_part_id).get(section_id)
  await client.assessment_tests.test_parts(test_id).sections(test_part_id).create({"identifier": identifier, **content})
  await client.assessment_tests.test_parts(test_id).sections(test_part_id).update(section_id, {**content})
  await client.assessment_tests.test_parts(test_id).sections(test_part_id).delete(section_id)
  ```
</CodeGroup>

### Section Items

Manage items within a section:

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.assessmentTests.testParts(testId).sections(testPartId).items(sectionId).add(itemRef)
  client.assessmentTests.testParts(testId).sections(testPartId).items(sectionId).remove(itemIdentifier)
  client.assessmentTests.testParts(testId).sections(testPartId).items(sectionId).reorder(orderInput)
  ```

  ```python Python theme={null}
  await client.assessment_tests.test_parts(test_id).sections(test_part_id).items(section_id).add(item_ref)
  await client.assessment_tests.test_parts(test_id).sections(test_part_id).items(section_id).remove(item_identifier)
  await client.assessment_tests.test_parts(test_id).sections(test_part_id).items(section_id).reorder(order_input)
  ```
</CodeGroup>

## Stimuli

Stimuli are shared content blocks referenced by multiple items:

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.stimuli.list()
  client.stimuli.get(stimulusId)
  client.stimuli.stream()
  client.stimuli.create({ identifier, title, ...content })
  client.stimuli.update(stimulusId, { title, ...content })
  client.stimuli.upsert(stimulusId, { title, ...content })
  client.stimuli.delete(stimulusId)
  ```

  ```python Python theme={null}
  await client.stimuli.list()
  await client.stimuli.get(stimulus_id)
  async for stimulus in client.stimuli.stream():
      ...
  await client.stimuli.create({"identifier": identifier, "title": title, **content})
  await client.stimuli.update(stimulus_id, {"title": title, **content})
  await client.stimuli.upsert(stimulus_id, {"title": title, **content})
  await client.stimuli.delete(stimulus_id)
  ```
</CodeGroup>

<Info>
  `upsert()` is available on all three writable QTI resources.

  * **`update(id, data)`**: strict (throws if the resource doesn't exist)
  * **`upsert(id, data)`**: loose (creates the resource if it doesn't exist)
</Info>

## Lessons

Submit and retrieve lesson feedback:

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.lesson.submitLesson({ lessonId, studentId, ...feedbackData })
  client.lesson.getLesson(lessonId)
  client.lesson.submitQuestion({ questionId, studentId, ...feedbackData })
  ```

  ```python Python theme={null}
  await client.lesson.submit_lesson({"lesson_id": lesson_id, "user_id": user_id, **feedback_data})
  await client.lesson.get_lesson(lesson_id)
  await client.lesson.submit_question({"question_id": question_id, "user_id": user_id, **feedback_data})
  ```
</CodeGroup>

## Validation

Validate QTI XML content:

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.validate.validate({ schema: 'item', xml: '<qti-assessment-item>...</qti-assessment-item>' })
  client.validate.validate({ schema: 'test', xml: '<qti-assessment-test>...</qti-assessment-test>' })
  client.validate.validate({ schema: 'stimulus', xml: '<qti-stimulus>...</qti-stimulus>' })
  client.validate.batch({
  	xml: ['<qti-assessment-item>...</qti-assessment-item>', '...'],
  	schema: 'item',
  	entityIds: ['item-1', 'item-2'],
  })
  ```

  ```python Python theme={null}
  await client.validate.validate({"schema": "item", "xml": "<qti-assessment-item>...</qti-assessment-item>"})
  await client.validate.validate({"schema": "test", "xml": "<qti-assessment-test>...</qti-assessment-test>"})
  await client.validate.validate({"schema": "stimulus", "xml": "<qti-stimulus>...</qti-stimulus>"})
  await client.validate.batch({
      "xml": ["<qti-assessment-item>...</qti-assessment-item>", "..."],
      "schema": "item",
      "entity_ids": ["item-1", "item-2"],
  })
  ```
</CodeGroup>

| Schema     | Description             |
| ---------- | ----------------------- |
| `item`     | QTI 3.0 assessment item |
| `test`     | QTI 3.0 assessment test |
| `stimulus` | QTI 3.0 stimulus        |

Returns `{ valid: boolean, errors?: string[] }`

## Item Types

QTI supports various interaction types:

| Type                      | Description                                 |
| ------------------------- | ------------------------------------------- |
| `choiceInteraction`       | Multiple choice (single or multiple select) |
| `textEntryInteraction`    | Text input                                  |
| `extendedTextInteraction` | Long text/essay                             |
| `inlineChoiceInteraction` | Dropdown select                             |
| `orderInteraction`        | Ordering/ranking                            |
| `matchInteraction`        | Matching pairs                              |
| `gapMatchInteraction`     | Fill in the blank                           |
| `hotspotInteraction`      | Image hotspots                              |

## Next Steps

<CardGroup cols={2}>
  <Card title="PowerPath" icon="road" href="/beta/build-on-timeback/clients/powerpath">
    Adaptive assessments
  </Card>

  <Card title="OneRoster" icon="users" href="/beta/build-on-timeback/clients/oneroster">
    Link assessments to gradebook
  </Card>

  <Card title="Types" icon="brackets-curly" href="/beta/api-reference/overview">
    QTI type definitions
  </Card>
</CardGroup>
