# Roster Sync

> A generic, LMS-agnostic recipe for keeping Grout people and groups in step with your SIS.

Canonical: https://grout.app/developer/documentation/guides/roster-sync/

## Data model

Keep three mappings on your side:

| Your record | Grout id | Where it comes from |
|---|---|---|
| Student | `users.id` + `login_email` | `POST /students` response |
| Teacher | `users.id` + `login_email` | `POST /faculty` response |
| Section | `groups.id` | `POST /groups` response |

Never derive the login email; always store what the API returned.

## Algorithm

```text
for each teacher in SIS:        ensure faculty  (POST /faculty if missing)
for each student in SIS:        ensure student  (POST /students or /students/bulk if missing)
                                PATCH /students/{id} if name/personal_email changed
for each section in SIS:        ensure group    (POST /groups if missing)
                                PATCH /groups/{id} { student_ids: [...] }   # replaces membership
for each student no longer enrolled anywhere: POST /students/{id}/suspend
```

## Idempotency and safety

- Provisioning is **not** idempotent; check your mapping before creating.
- `PATCH /groups/{id}` with `student_ids` is a full replace — compute the list from the SIS, do not merge.
- Suspend, don't delete. Suspended students keep their license and history.
- Run under a key with only `students:write`, `faculty:write`, `groups:write`.

## Verifying a run

```bash
curl "$API/students?status=pending&limit=200" -H "Authorization: Bearer $KEY"   # never signed in
curl $API/licenses/summary -H "Authorization: Bearer $KEY"
curl "$API/events?type=student.*&since=<run start>" -H "Authorization: Bearer $KEY"
```

## First-run tips

- Pre-map `personal_email` so students skip the mapping screen on first login.
- Send students their `login_email` from your own system; the API does not email students on creation.
- Faculty first logins route the OTP to the institution admin's mailbox until the teacher completes their profile.
