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

# OneRoster

> OneRoster 1.2 API client for rostering, enrollments, and gradebook

## Overview

The OneRoster client provides access to the OneRoster 1.2 API, supporting:

* **Rostering**: Users, orgs, schools, courses, classes, enrollments
* **Gradebook**: Categories, line items, results, score scales
* **Assessment**: Assessment line items and results
* **Resources**: Learning resources linked to courses

## Installation

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

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

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

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

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

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

## Quick Start

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

  const client = new OneRosterClient({
      env: 'staging',
      auth: {
          clientId: process.env.ONEROSTER_CLIENT_ID!,
          clientSecret: process.env.ONEROSTER_CLIENT_SECRET!,
      },
  })

  const { data: students, hasMore } = await client.users.list({ where: { role: 'student' } })
  const user = await client.users.get('user-123')
  ```

  ```python Python theme={null}
  import os
  from timeback_oneroster import OneRosterClient

  client = OneRosterClient(
      env="staging",
      client_id=os.environ["ONEROSTER_CLIENT_ID"],
      client_secret=os.environ["ONEROSTER_CLIENT_SECRET"],
  )

  students = await client.users.list(where={"role": "student"})
  user = await client.users.get("user-123")
  ```
</CodeGroup>

## Rostering

### Users

Payload variables such as `userInput` below must contain all required fields. User creation requires `sourcedId`, `enabledUser`, `givenName`, `familyName`, `email`, and a nonempty `roles` array; a top-level `role` is not the create contract. Python write methods accept one payload dictionary using API field names, not arbitrary keyword arguments.

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.users.list()
  client.users.list({ where: { role: 'teacher' } })
  client.users.first({ where: { role: 'administrator' } })
  client.users.listAll()
  client.users.get(userId)
  client.users.exists(userId)
  client.users.create(userInput)
  client.users.update(userId, { givenName })
  client.users.upsert(userId, userInput)
  client.users.delete(userId)
  ```

  ```python Python theme={null}
  await client.users.list()
  await client.users.list(where={"role": "teacher"})
  await client.users.first(where={"role": "administrator"})
  await client.users.list_all()
  await client.users.get(user_id)
  await client.users.create(user_input)
  await client.users.update(user_id, {"givenName": given_name})
  await client.users.delete(user_id)
  ```
</CodeGroup>

| Method | Returns | Description |
| - | - | - |
| `list()` | `PageResult` | List users (supports `where` filter) |
| `first()` | `User \| undefined` | Get first matching user |
| `listAll()` | `User[]` | Fetch all pages |
| `get()` | `User` | Get user by ID |
| `exists()` | `boolean` | Check if user exists (lightweight) |
| `create()` | `CreateResponse` | Create user (returns `sourcedIdPairs`) |
| `update()` | `void` | Update user (throws if not found) |
| `upsert()` | `void` | Create or update user |
| `delete()` | `void` | Delete user |

<Info>
  `exists()`, `update()`, and `upsert()` are available on all writable OneRoster resources.

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

### Scoped User Operations

Access user-specific data using the callable pattern:

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.users('user-123').demographics()
  client.users('user-123').classes()
  client.users('user-123').resources()
  client.users('user-123').agents()
  client.users('user-123').agentFor()
  client.users('user-123').addAgent({ sourcedId: 'parent-123', role: 'parent' })
  client.users('user-123').removeAgent('parent-123')
  client.users('user-123').registerCredential({ applicationName, credentials: { username, password } })
  client.users('user-123').decryptCredential('cred-id')
  ```

  ```python Python theme={null}
  await client.users("user-123").demographics()
  await client.users("user-123").classes()
  await client.users("user-123").resources()
  await client.users("user-123").agents()
  await client.users("user-123").agent_for()
  await client.users("user-123").add_agent({"sourcedId": "parent-123", "role": "parent"})
  await client.users("user-123").remove_agent("parent-123")
  await client.users("user-123").register_credential({"applicationName": application_name, "credentials": {"username": username, "password": password}})
  await client.users("user-123").decrypt_credential("cred-id")
  ```
</CodeGroup>

### Students & Teachers

Dedicated read-only resources for role-specific queries:

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.students.list()
  client.students.get(studentId)
  client.students(studentId).classes()
  client.teachers.list()
  client.teachers.get(teacherId)
  client.teachers(teacherId).classes()
  ```

  ```python Python theme={null}
  await client.students.list()
  await client.students.get(student_id)
  await client.students(student_id).classes()
  await client.teachers.list()
  await client.teachers.get(teacher_id)
  await client.teachers(teacher_id).classes()
  ```
</CodeGroup>

### Schools & Organizations

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.schools.list()
  client.schools.get(schoolId)
  client.orgs.list()

  // Nested resources
  client.schools('school-id').classes()
  client.schools('school-id').courses()
  client.schools('school-id').terms()
  client.schools('school-id').teachers()
  client.schools('school-id').students()
  client.schools('school-id').enrollments()
  client.schools('school-id').scoreScales()
  client.schools('school-id').lineItems()

  // Deeply nested
  client.schools('school-id').class('class-id').teachers()
  client.schools('school-id').class('class-id').students()
  client.schools('school-id').class('class-id').enrollments()
  ```

  ```python Python theme={null}
  await client.schools.list()
  await client.schools.get(school_id)
  await client.orgs.list()

  # Nested resources
  await client.schools("school-id").classes()
  await client.schools("school-id").courses()
  await client.schools("school-id").terms()
  await client.schools("school-id").teachers()
  await client.schools("school-id").students()
  await client.schools("school-id").enrollments()
  await client.schools("school-id").score_scales()
  await client.schools("school-id").line_items()
  ```
</CodeGroup>

### Courses & Classes

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.courses.list()
  client.courses(courseId).classes()
  client.courses(courseId).resources()
  client.courses.components()
  client.courses.getComponent(componentId)
  client.courses.componentResources()
  client.courses.getComponentResource(resourceId)
  client.courses.createStructure(courseStructureInput)

  client.classes.get(classId)
  client.classes(classId).students()
  client.classes(classId).teachers()
  client.classes(classId).lineItems()
  client.classes(classId).results()
  client.classes(classId).categories()
  client.classes(classId).scoreScales()
  client.classes(classId).resources()
  client.classes(classId).enroll({ sourcedId: userId, role: 'student' })

  // Deeply nested gradebook
  client.classes(classId).lineItem(lineItemId).results()
  client.classes(classId).student(studentId).results()
  ```

  ```python Python theme={null}
  await client.courses.list()
  await client.courses(course_id).classes()
  await client.courses(course_id).resources()
  await client.courses(course_id).components()

  await client.classes.get(class_id)
  await client.classes(class_id).students()
  await client.classes(class_id).teachers()
  await client.classes(class_id).enrollments()
  await client.classes(class_id).line_items()
  await client.classes(class_id).results()
  await client.classes(class_id).categories()
  await client.classes(class_id).score_scales()
  await client.classes(class_id).resources()
  await client.classes(class_id).enroll({"sourcedId": user_id, "role": "student"})

  # Deeply nested gradebook
  await client.classes(class_id).line_item(line_item_id).results()
  await client.classes(class_id).student(student_id).results()
  ```
</CodeGroup>

### Enrollments

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.enrollments.list()
  client.enrollments.list({ where: { status: 'active' } })
  client.enrollments.get(enrollmentId)
  client.enrollments.create({ user, class: classRef, role })
  client.enrollments.patch(enrollmentId, { status: 'tobedeleted' })
  ```

  ```python Python theme={null}
  await client.enrollments.list()
  await client.enrollments.list(where={"status": "active"})
  await client.enrollments.get(enrollment_id)
  await client.enrollments.create({"user": user_ref, "class": class_ref, "role": "student"})
  await client.enrollments.patch(enrollment_id, {"status": "tobedeleted"})
  ```
</CodeGroup>

### Terms & Academic Sessions

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.terms.list()
  client.terms(termId).classes()
  client.terms(termId).gradingPeriods()
  client.academicSessions.list()
  client.gradingPeriods.list()
  ```

  ```python Python theme={null}
  await client.terms.list()
  await client.terms(term_id).classes()
  await client.terms(term_id).grading_periods()
  await client.academic_sessions.list()
  await client.grading_periods.list()
  ```
</CodeGroup>

### Demographics

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.demographics.list()
  client.demographics.get(demographicId)
  ```

  ```python Python theme={null}
  await client.demographics.list()
  await client.demographics.get(demographic_id)
  ```
</CodeGroup>

## Gradebook

Gradebook resources are top-level on the client:

<CodeGroup>
  ```typescript TypeScript theme={null}
  // Core reads
  await client.categories.list()
  await client.lineItems.get(lineItemId)
  await client.results.list()

  // Gradebook writes return updated entities
  const updatedCategory = await client.categories.update(categoryId, { title: 'Homework 2.0' })
  const upsertedLineItem = await client.lineItems.upsert(lineItemId, lineItemInput)

  // Assessment writes also return updated entities
  const upsertedAssessmentResult = await client.assessmentResults.upsert(
      assessmentResultId,
      assessmentResultInput,
  )
  ```

  ```python Python theme={null}
  await client.categories.list()
  await client.categories.get(category_id)
  await client.line_items.list()
  await client.line_items.get(line_item_id)
  await client.line_items.create(line_item_input)
  await client.line_items(line_item_id).results()
  await client.results.list()
  await client.results.create(result_input)
  await client.score_scales.list()

  # Assessment
  await client.assessment_line_items.list()
  await client.assessment_results.list()
  ```
</CodeGroup>

<Info>
  For **gradebook + assessment** resources, write methods return the entity:

  * **`update(id, data)`**: strict (throws on 404), returns entity
  * **`upsert(id, data)`**: loose (creates/updates), returns entity

  For **rostering + resources**, write methods remain `void`.
</Info>

| Resource family | `update()` / `upsert()` return |
| - | - |
| Rostering + Resources | `void` |
| Gradebook + Assessment | Updated entity |

## Resources

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.resources.list()
  client.resources.create(resourceInput)
  client.resources('resource-id').export()
  ```

  ```python Python theme={null}
  await client.resources.list()
  await client.resources.create(resource_input)
  await client.resources("resource-id").export()
  ```
</CodeGroup>

## Pagination

Top-level paginated resource `list()` methods return `PageResult<T>` with `{ data, hasMore, total, nextOffset }`.

<CodeGroup>
  ```text TypeScript — API notation (not executable) theme={null}
  client.users.list({ offset?, limit? })
  client.users.listAll(options?)
  client.users.stream(options?)
  ```

  ```python Python theme={null}
  await client.users.list(offset=..., limit=...)
  await client.users.list_all()
  async for user in client.users.stream():
      ...
  ```
</CodeGroup>

| Method | Returns | Description |
| - | - | - |
| `list()` | `PageResult<T>` | Single page with pagination info |
| `listAll()` | `T[]` | Auto-fetches all pages |
| `stream()` | `AsyncIterable<T>` | Memory-efficient streaming iterator |

## Filtering

Use the `where` parameter to filter results:

<CodeGroup>
  ```typescript TypeScript theme={null}
  client.users.list({ where: { role: 'student' } })
  client.enrollments.list({ where: { status: 'active' } })
  client.users.list({ where: { role: 'student', status: 'active' } })
  ```

  ```python Python theme={null}
  await client.users.list(where={"role": "student"})
  await client.enrollments.list(where={"status": "active"})
  await client.users.list(where={"role": "student", "status": "active"})
  ```
</CodeGroup>

<Note>Enrollment PUT requires a full enrollment payload. Use `patch(id, data)` for a partial status/date/metadata change. A scoped user credential registration requires `applicationName` plus both `username` and `password` under `credentials`.</Note>

## Error Handling

```typescript theme={null}
try {
    const user = await client.users.get('invalid-id')
} catch (error) {
    if (error instanceof Error) {
        console.error(error.message)
        if ('statusCode' in error && typeof error.statusCode === 'number') {
            console.error(error.statusCode)
        }
    }
}
```

## Next Steps

<CardGroup cols={2}>
  <Card title="EduBridge" icon="bridge" href="/beta/build-on-timeback/clients/edubridge">
    Simplified analytics queries
  </Card>

  <Card title="Caliper" icon="chart-line" href="/beta/build-on-timeback/clients/caliper">
    Learning event tracking
  </Card>

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