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

<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({ givenName, familyName, email, role })
  client.users.update(userId, { givenName })
  client.users.upsert(userId, { givenName, familyName, email, role })
  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(given_name=..., family_name=..., email=..., role=...)
  await client.users.update(user_id, 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').createCredential({ type: 'username', username: 'johndoe' })
  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").create_credential({"type": "username", "username": "johndoe"})
  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({ ... })

  client.classes.get(classId)
  client.classes(classId).students()
  client.classes(classId).teachers()
  client.classes(classId).enrollments()
  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, 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=..., class_=..., role=...)
  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(title=..., class_=..., category=..., assign_date=..., due_date=...)
  await client.line_items(line_item_id).results()
  await client.results.list()
  await client.results.create(line_item=..., student=..., score=..., score_status=...)
  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({ ... })
  client.resources('resource-id').export()
  ```

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

## Pagination

All `list()` methods return `PageResult<T>` with `{ data, hasMore, total, nextOffset }`.

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

## Error Handling

```typescript theme={null}
import { OneRosterError } from '@timeback/oneroster'

try {
	const user = await client.users.get('invalid-id')
} catch (error) {
	if (error instanceof OneRosterError) {
		console.log(error.statusCode) // 404
		console.log(error.message) // 'Not Found'
	}
}
```

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