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:
- Register your app (see Level 0: Registration)
- Request OAuth credentials with the
roster.createputscope (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,relativeaide,counselor,principal,proctordistrictAdministrator,siteAdministrator,systemAdministrator
Step 7: Link a Student to an Agent
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}. Theroster-demographics.readonlyscope must be requested in addition toroster.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' },
...
}
}
Related Docs
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.
