Level 1: Student Onboarding

Programmatically create and manage organizations, students, users, and their relationships in TimeBack's roster.

What Student Onboarding Provides

Student onboarding enables your learning app to:

  • Query organizations (schools, districts) in TimeBack
  • Fetch users and students belonging to a specific organization
  • Create and update student records in TimeBack's roster
  • Manage user accounts with roles and profiles
  • Link students to agents (parents, guardians, relatives)
  • Query agent relationships for a user

This is foundational for apps that need to manage student data programmatically rather than relying solely on LTI authentication or manual registration.

Prerequisites

Before implementing student onboarding:

  1. Register your app (see Level 0: Registration)
  2. Request OAuth credentials with the roster.createput scope (see Authentication Guide)

Implementation Guide

Step 1: Obtain Access Token

Request an access token with roster write permissions:

const response = await fetch('https://platform.timeback.com/auth/1.0/token', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    Authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
  },
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    scope: 'https://purl.imsglobal.org/spec/or/v1p2/scope/roster.createput',
  }),
});

const { access_token } = await response.json();

Step 2: Fetch Organizations

Retrieve all organizations (schools, districts) available in TimeBack:

GET https://platform.timeback.com/rostering/1.0/orgs

Example request:

const response = await fetch('https://platform.timeback.com/rostering/1.0/orgs?limit=100', {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
});

const { orgs, total } = await response.json();

Example response:

{
  "orgs": [
    {
      "sourcedId": "org-uuid-123",
      "status": "active",
      "dateLastModified": "2025-01-02T10:00:00Z",
      "name": "Springfield Elementary",
      "type": "school",
      "identifier": "SPR-001",
      "parent": {
        "sourcedId": "district-uuid-456",
        "type": "org"
      },
      "children": []
    }
  ],
  "offset": 0,
  "limit": 100,
  "total": 1
}

Key response fields:

Field Type Description
sourcedId string Unique identifier for the organization
name string Display name of the organization
type string school, district, department, local, state, national
identifier string External identifier (e.g., school code)
parent object Reference to parent organization (null for top-level)
children array References to child organizations

Step 3: Fetch a Specific Organization

Retrieve details for a single organization by ID:

GET https://platform.timeback.com/rostering/1.0/orgs/{sourcedId}

Example request:

const orgId = 'org-uuid-123';
const response = await fetch(`https://platform.timeback.com/rostering/1.0/orgs/${orgId}`, {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
});

const { organization } = await response.json();

Step 4: Fetch Users by Organization

Retrieve all users belonging to a specific organization using filtering:

GET https://platform.timeback.com/rostering/1.0/users?filter=primaryOrg.sourcedId='{orgId}'

Example request:

const orgId = 'org-uuid-123';
const filter = encodeURIComponent(`primaryOrg.sourcedId='${orgId}'`);

const response = await fetch(`https://platform.timeback.com/rostering/1.0/users?filter=${filter}&limit=100`, {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
});

const { users, total } = await response.json();

Filtering by role:

To fetch only students for an organization, combine filters:

const filter = encodeURIComponent(`primaryOrg.sourcedId='${orgId}' AND roles='student'`);

See OneRoster API Conventions for more filtering, pagination, and sorting options.

Step 5: Create or Update a Student

Use the upsertStudent endpoint to create a new student or update an existing one (requires the organization sourcedId from Step 2 or 3):

PUT https://platform.timeback.com/rostering/1.0/students

Example request:

const studentData = {
  student: {
    sourcedId: 'student-uuid-here', // Your unique identifier for this student
    status: 'active',
    username: 'john.doe',
    enabledUser: 'true',
    givenName: 'John',
    familyName: 'Doe',
    middleName: null,
    email: 'john.doe@example.com',
    phone: null,
    grades: ['5'],
    demographics: {
      birthDate: '2010-05-12', // ISO date; optional
    },
    primaryOrg: {
      sourcedId: 'organization-uuid', // The school/org this student belongs to
      type: 'org',
    },
  },
};

const response = await fetch('https://platform.timeback.com/rostering/1.0/students', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${accessToken}`,
  },
  body: JSON.stringify(studentData),
});

// Returns 200 OK on success

Required fields:

Field Type Description
sourcedId string Unique identifier for the student
status string active, inactive, or tobedeleted
username string Login username
enabledUser string "true" or "false" (as strings)
givenName string Student's first name
familyName string Student's last name
primaryOrg object Reference to the student's primary organization
grades string[] Array of grade levels (e.g., ["5", "6"])
demographics object Optional. Demographic record for the student. Today only birthDate is accepted on upsert; the full demographic field set is exposed read-only via Step 11.

Demographics writes flow through this endpoint as an optional demographics object on the body. Today only birthDate is accepted on upsert; the full record (sex, ethnicity flags, country / state / city of birth, public-school residence status) is exposed read-only via the dedicated demographics endpoint covered in Step 11.

Step 6: Create or Update a User

For non-student users (teachers, parents, administrators), use the upsertUser endpoint:

PUT https://platform.timeback.com/rostering/1.0/users/{sourcedId}

Example request:

const userData = {
  user: {
    sourcedId: 'user-uuid-here',
    status: 'active',
    enabledUser: 'true',
    givenName: 'Jane',
    familyName: 'Smith',
    email: 'jane.smith@example.com',
    grades: [],
    demographics: {
      birthDate: '1980-03-14', // ISO date; optional
    },
    primaryOrg: {
      sourcedId: 'organization-uuid',
      type: 'org',
    },
    roles: [
      {
        roleType: 'primary',
        role: 'parent',
        org: {
          sourcedId: 'organization-uuid',
          type: 'org',
        },
      },
    ],
  },
};

const response = await fetch(`https://platform.timeback.com/rostering/1.0/users/${userData.user.sourcedId}`, {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${accessToken}`,
  },
  body: JSON.stringify(userData),
});

// Returns 201 Created on success

As with Step 5, an optional demographics object can be included on the body. Today only birthDate is accepted on upsert; the full record (sex, ethnicity flags, country / state / city of birth, public-school residence status) is exposed read-only via the dedicated demographics endpoint covered in Step 11.

Available roles:

  • student, teacher, parent, guardian, relative
  • aide, counselor, principal, proctor
  • districtAdministrator, siteAdministrator, systemAdministrator

Create relationships between students and their agents (parents, guardians):

PUT https://platform.timeback.com/rostering/1.0/students/{sourcedId}/agents/{agentId}

Path parameters:

Parameter Type Description
sourcedId string Unique identifier of the student
agentId string A client-generated UUID that uniquely identifies this agent relationship. Use this same ID to update or delete the relationship later.

Example request:

const agentData = {
  user: {
    sourcedId: 'parent-user-uuid', // The agent's user ID
    type: 'user',
  },
  relationshipType: 'parent', // free-form string describing the relationship
};

// Generate a UUID for the agent relationship (client-generated)
const agentRelationshipId = crypto.randomUUID();

const response = await fetch(
  `https://platform.timeback.com/rostering/1.0/students/${studentId}/agents/${agentRelationshipId}`,
  {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${accessToken}`,
    },
    body: JSON.stringify(agentData),
  },
);

// Returns 201 Created on success

Relationship type:

The relationshipType is a free-form string. Use any value that describes the relationship in your system.

Step 8: Query Linked Users

Retrieve bidirectional agent relationships for a user. This endpoint shows both:

  • agentsAsSource: Users who are agents of the queried user (e.g., a student's parents/guardians)
  • agentsAsAgent: Users for whom the queried user is an agent (e.g., a parent's children)
GET https://platform.timeback.com/rostering/1.0/users/{sourcedId}/linked-users

Example request:

const response = await fetch(`https://platform.timeback.com/rostering/1.0/users/${userId}/linked-users`, {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${accessToken}`,
  },
});

const data = await response.json();

Example response:

{
  "agentsAsSource": [
    {
      "agentId": "agent-relationship-uuid",
      "relationshipType": "parent",
      "userId": "parent-user-uuid"
    }
  ],
  "agentsAsAgent": [
    {
      "agentId": "agent-relationship-uuid-2",
      "relationshipType": "guardian",
      "userId": "child-user-uuid"
    }
  ]
}

Response fields:

Field Type Description
agentsAsSource array Users who are agents of the queried user (e.g., parents)
agentsAsAgent array Users for whom the queried user is an agent (e.g., children)
agentId string Unique identifier for this agent relationship (use for update/delete)
relationshipType string The type of relationship (e.g., "parent", "guardian")
userId string The sourcedId of the linked user

Step 9: Query User Agents

Retrieve all agents associated with a user (e.g., get a student's parents):

GET https://platform.timeback.com/rostering/1.0/users/{sourcedId}/agents

Example request:

const response = await fetch(`https://platform.timeback.com/rostering/1.0/users/${userId}/agents`, {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${accessToken}`,
  },
});

const data = await response.json();
// Returns: { agents: [{ agentId, user: { sourcedId, type }, relationshipType }] }

Example response:

{
  "agents": [
    {
      "agentId": "agent-relationship-uuid",
      "user": {
        "sourcedId": "parent-user-uuid",
        "type": "user"
      },
      "relationshipType": "parent"
    }
  ]
}

Response fields:

Field Type Description
agentId string Unique identifier for this agent relationship (use for update/delete)
user object Reference to the agent user with sourcedId and type
relationshipType string The type of relationship (e.g., "parent", "guardian")

Step 10: Remove an Agent Relationship

Delete an agent relationship when it's no longer valid:

DELETE https://platform.timeback.com/rostering/1.0/students/{sourcedId}/agents/{agentId}

Path parameters:

Parameter Type Description
sourcedId string Unique identifier of the student
agentId string The UUID of the agent relationship to delete (same ID used when creating the relationship)

Example request:

// Use the agentId from getUserAgents or getUserLinkedUsers response
const response = await fetch(
  `https://platform.timeback.com/rostering/1.0/students/${studentId}/agents/${agentRelationshipId}`,
  {
    method: 'DELETE',
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  },
);

// Returns 204 No Content on success

Step 11: Read User Demographics

TimeBack exposes a dedicated read surface for demographics. Use it when your integration needs the full demographic record — birth date, sex, race / ethnicity flags, country / state / city of birth, and public-school residence status — typically for compliance reporting, equity analytics, or eligibility checks. Reads require the https://purl.imsglobal.org/spec/or/v1p2/scope/roster-demographics.readonly scope to be requested in addition to roster.readonly. Note that GET /rostering/1.0/users/{sourcedId} does NOT include demographic fields; the endpoint below is the only read path.

Fetch Demographics by User

Given a user's sourcedId (the value you used when calling upsertUser / upsertStudent), call the dedicated user-keyed endpoint:

GET https://platform.timeback.com/rostering/1.0/users/{sourcedId}/demographics

The path parameter is the user's sourcedId, NOT a demographic record's own sourcedId. The response unwraps the single record directly. Returns 404 if no demographic is linked to the user.

Example request:

const userId = 'student-uuid-here';
const response = await fetch(`https://platform.timeback.com/rostering/1.0/users/${userId}/demographics`, {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
});

const { demographics } = await response.json();

Example response (a real platform response for user 43e1b53c-3c97-44f7-981a-9d4e74b8bfcf):

{
  "demographics": {
    "sourcedId": "d181d16e-65a0-407a-ab11-a969e25a1570",
    "status": "active",
    "dateLastModified": "2025-07-16T10:08:59.880Z",
    "metadata": {},
    "birthDate": "2020-02-16",
    "sex": null,
    "countryOfBirthCode": null,
    "stateOfBirthAbbreviation": null,
    "cityOfBirth": null,
    "publicSchoolResidenceStatus": null
  }
}

Demographic records are 1:1 with users. The returned sourcedId is the demographic record's own auto-generated UUID — distinct from the user's sourcedId you passed in. Fields that are unset on the underlying record may be omitted from the JSON payload entirely (as with the race / ethnicity flags above) rather than serialized as null; consumers should treat absent fields as unknown. The complete field shape is documented in the table below.

The fields query parameter is supported to limit the returned attributes. See OneRoster API Conventions for details.

Response field reference

Field Type Description
sourcedId string Unique identifier of the demographic record (its own UUID, distinct from the user's sourcedId)
status string active, inactive, or tobedeleted
dateLastModified string ISO 8601 timestamp of the last update
metadata object Free-form additional metadata, or null
birthDate string ISO 8601 date (YYYY-MM-DD), or null
sex string male, female, other, unspecified, or null
americanIndianOrAlaskaNative string OneRoster boolean: "true" / "false" / null
asian string OneRoster boolean: "true" / "false" / null
blackOrAfricanAmerican string OneRoster boolean: "true" / "false" / null
nativeHawaiianOrOtherPacificIslander string OneRoster boolean: "true" / "false" / null
white string OneRoster boolean: "true" / "false" / null
demographicRaceTwoOrMoreRaces string OneRoster boolean: "true" / "false" / null
hispanicOrLatinoEthnicity string OneRoster boolean: "true" / "false" / null
countryOfBirthCode string ISO country code, or null
stateOfBirthAbbreviation string State / region abbreviation, or null
cityOfBirth string City of birth, or null
publicSchoolResidenceStatus string Free-form residence status string, or null

Important: Demographic fields are NOT returned by GET /rostering/1.0/users/{sourcedId}. The roster-demographics.readonly scope must be requested in addition to roster.readonly — it is not a replacement.

Required Scopes

The getDemographicsForUser operation below requires the roster-demographics.readonly scope in addition to the base roster.readonly, not as a replacement.

Operation Required Scope
getAllOrganizations https://purl.imsglobal.org/spec/or/v1p2/scope/roster.readonly
getOrganization https://purl.imsglobal.org/spec/or/v1p2/scope/roster.readonly
getAllUsers https://purl.imsglobal.org/spec/or/v1p2/scope/roster.readonly
upsertStudent https://purl.imsglobal.org/spec/or/v1p2/scope/roster.createput
upsertUser https://purl.imsglobal.org/spec/or/v1p2/scope/roster.createput
upsertStudentAgent https://purl.imsglobal.org/spec/or/v1p2/scope/roster.createput
deleteStudentAgent https://purl.imsglobal.org/spec/or/v1p2/scope/roster.createput
getUserAgents https://purl.imsglobal.org/spec/or/v1p2/scope/roster.readonly
getUserLinkedUsers https://purl.imsglobal.org/spec/or/v1p2/scope/roster.readonly
getDemographicsForUser https://purl.imsglobal.org/spec/or/v1p2/scope/roster-demographics.readonly

Common Pitfalls

Not matching sourcedId in path and body

// ❌ Bad: Path and body sourcedId don't match
await fetch('/rostering/1.0/users/user-123', {
  body: JSON.stringify({ user: { sourcedId: 'user-456', ... } }),
});

// ✅ Good: Path and body sourcedId match
await fetch('/rostering/1.0/users/user-123', {
  body: JSON.stringify({ user: { sourcedId: 'user-123', ... } }),
});

Using boolean instead of string for enabledUser

// ❌ Bad: Boolean value
{
  enabledUser: true;
}

// ✅ Good: String value
{
  enabledUser: 'true';
}

Missing required organization reference

// ❌ Bad: Missing primaryOrg
{ student: { sourcedId: '...', givenName: 'John', ... } }

// ✅ Good: Include primaryOrg
{
  student: {
    sourcedId: '...',
    givenName: 'John',
    primaryOrg: { sourcedId: 'org-uuid', type: 'org' },
    ...
  }
}

Level 0: Registration

Register your app first to get OAuth credentials for API access.

Level 2: LTI Launch

Implement seamless authentication so students can access your app without credentials.

Level 3: Caliper Events

Send learning activity events to unlock coaching insights and unified analytics.

OneRoster API Conventions

Filtering, pagination, sorting, and field selection for roster APIs.

Authentication

Detailed guide on obtaining and managing OAuth access tokens.