Session
The signed-in user, the schools and roles they hold, and post-registration set-up.
GET/me
Get the current user and their school memberships
Returns every membership the caller has (a parent with children at two schools has two), with enough per-membership detail for the client to build its own school switcher and decide what to render. There is no server-side active school: the client keeps its own choice. is_fully_deactivated is true when every membership of the account is deactivated; the client should then show an account-deactivated screen instead of the roster.
Responses
200
application/jsonMeResponseOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.
POST/registration/confirm
Complete registration after email confirmation
Call once after a freshly confirmed sign-up, for example when the user opens the confirmation link in your app. It looks the caller's verified email up in the school invitations (the registration allowlist), sets up the matching school memberships and guardian links, and returns what was set up so the client can show a "here is what we found" confirmation screen.
Responses
200
application/jsonRegistrationConfirmResultProvisioning complete (or already had been, safe to call twice)
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found
Categories
Per-school content categories shared by Documents and Notices (read-only here).
GET/schools/{schoolId}/categories
List the school's content categories
The per-school category vocabulary shared by Documents and Notices, in the order a Senior Admin arranged them. Any school member may read (parents need names/colors to render pills and filter chips). Not feature-gated.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description categoriesarray of CategoryYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
Directory
Roster, student detail, map markers and stars.
GET/schools/{schoolId}/students
List the roster
Every non-archived student at the school, sorted by last name, then first name. q matches first or last name (partial, case-insensitive); a 4-digit q also matches the graduation year exactly, and for admin callers it also searches linked parents' name, phone and email (it adds nothing for a parent caller). The full result set is always returned: there is no pagination yet, and one may be added in a future minor version without a new API version. full=true returns RosterStudentFull items instead of RosterStudent; they add latitude/longitude, the caller's own note and full guardian contact (id, email and phone, not just name and relation), so a client can cache the whole roster in one round trip. full=true ignores q and graduationYear.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
q | query | string | No | Free-text search across name (and, for admins, linked parent contact info). Ignored when |
graduationYear | query | integer | No | Exact-match filter. Ignored when |
full | query | string, one of "true" | No | When |
Responses
200
application/jsonobjectOK
Field Type Required Description studentsarray of RosterStudentorRosterStudentFullYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
GET/schools/{schoolId}/map-markers
List geocoded student pins for the map view
Every non-archived, geocoded student, except: for a non-admin (parent) caller, a student's pin is dropped when that student's address_visible is false. The caller's own linked students are never suppressed, and admins always see every geocoded student. Coordinates are computed by the server, so a client never needs to geocode. Supplying org, list or starred narrows the response to that one group's students instead of the whole school; an unknown or other-school organization, an archived organization for a non-admin, or another user's list is a 404. The visibility rule above still applies within the scoped set, so scoping never reveals a pin the caller could not otherwise see. Supplying none of the three returns the whole-school response.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
org | query | string | No | Organization |
list | query | string (uuid) | No | Saved list id; narrows the response to that list's members. Owner-scoped. |
starred | query | string | No | Any non-empty value narrows the response to the caller's starred students. |
Responses
200
application/jsonobjectOK
Field Type Required Description markersarray of MapMarkerYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
GET/students/{studentId}
Get student detail
notes is always scoped to the caller's own notes on this student — never another guardian's or an admin's.
If the student has no coordinates yet (latitude/longitude both null) but has a full address, this endpoint geocodes on the caller's behalf inline and returns the freshly computed coordinates in the same response — the client should not attempt this itself.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
studentId | path | string (uuid) | Yes |
Responses
200
application/jsonStudentDetailOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
POST/students/{studentId}/star
Star a student
Private per-user. Idempotent — starring an already-starred student is a no-op success.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
studentId | path | string (uuid) | Yes |
Responses
204
Starred
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.
DELETE/students/{studentId}/star
Unstar a student
Idempotent — unstarring a not-starred student is a no-op success.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
studentId | path | string (uuid) | Yes |
Responses
204
Unstarred
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.
GET/schools/{schoolId}/guardians
List every guardian at the school
The whole-school guardian directory, for a client-side cache behind "add to list" pickers. Any school member may call it. Each row is one person: registered (is_registered: true, and the id is the membership id) or pending (the id is the invitation id). Fields the caller may not see (for example a parent who chose to hide their contact details) come back null.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description guardiansarray of object Yes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
Notes
Private notes a guardian keeps on a student. Never shared with other guardians.
GET/students/{studentId}/note
Get the caller's own note on a student
Returns note: null if the caller hasn't left one yet.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
studentId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description noteNoteYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.
PUT/students/{studentId}/note
Save (create/update/clear) the caller's own note
Upserts the caller's note keyed by (student, author). A blank/empty body deletes the note instead of storing an empty row - the response's note is null in that case.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
studentId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
body | string | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description noteNoteYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.
Lists
A user's private custom lists of students and parents.
GET/schools/{schoolId}/lists
List the caller's own custom lists at a school
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description listsarray of StudentListYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.
POST/schools/{schoolId}/lists
Create a list
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | |
description | string, nullable | No |
Responses
201
application/jsonStudentListCreated
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.
GET/lists/{listId}
Get a list and its students
Returns 404 (not 403) if the list doesn't belong to the caller — ownership is not revealed.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
listId | path | string (uuid) | Yes |
Responses
200
application/jsonStudentListDetailOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
PATCH/lists/{listId}
Rename a list
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
listId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes |
Responses
200
application/jsonStudentListOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
DELETE/lists/{listId}
Delete a list
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
listId | path | string (uuid) | Yes |
Responses
204
Deleted
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
POST/lists/{listId}/students
Add a student to a list
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
listId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
student_id | string (uuid) | Yes |
Responses
204
Added
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
DELETE/lists/{listId}/students/{studentId}
Remove a student from a list
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
listId | path | string (uuid) | Yes | |
studentId | path | string (uuid) | Yes |
Responses
204
Removed
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
GET/lists/{listId}/guardians
List a list's individual parent/guardian entries
Owner-only, same as every other list route.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
listId | path | string (uuid) | Yes |
Responses
200
application/jsonarray ofListGuardianOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
POST/lists/{listId}/guardians
Add a parent/guardian directly to a list
Not derived from a student being on the list. linked_student_id picks which of the parent's students to source an address from for the row.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
listId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
guardian_membership_id | string (uuid) | Yes | |
linked_student_id | string (uuid), nullable | No |
Responses
201
application/jsonListGuardianCreated
400
application/jsonErrorValidation failed: a field is missing or invalid.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
DELETE/lists/{listId}/guardians/{guardianId}
Remove a parent/guardian entry from a list
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
listId | path | string (uuid) | Yes | |
guardianId | path | string (uuid) | Yes |
Responses
204
Removed
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
GET/lists/{listId}/message-recipients
Resolved "Message Everyone" recipient list for a list, including phone
The resolved "Message Everyone" recipient list for one of the caller's own lists, including phone numbers (masked by each guardian's privacy settings). Owner only.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
listId | path | string (uuid) | Yes |
Responses
200
application/jsonarray ofMessageRecipientOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
GET/lists/{listId}/candidates
Minimal candidate list for the "add a parent" picker (list owner only)
Mirrors GET /organizations/{organizationId}/candidates - a parent adding another parent to their own list needs to search among every parent at the school, the same "any school member, not just my own" scope adding a student already has.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
listId | path | string (uuid) | Yes |
Responses
200
application/jsonarray ofOrganizationCandidateOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
Profile
The signed-in user's own contact information and visibility settings, and leaving a school.
GET/me/contact
Get the caller's own parent contact info
There is no separate guardian-profile table — a parent's name/phone is stored on their registration invitation(s), keyed by verified email, and this reads across every school/ student the caller is linked to, picking the most recently created row that actually has data filled in.
Responses
200
application/jsonMyContactOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.
PATCH/me/contact
Update the caller's own parent contact info
Updates the caller's own parent contact information. The change applies to every school and student the parent is linked to at once, because a parent has one identity across schools, matched by their verified email.
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
first_name | string | Yes | |
last_name | string | Yes | |
phone | string | Yes |
Responses
200
application/jsonMyContactOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.
PATCH/me/profile-visibility
Set the caller's own contact visibility
Sets whether the caller's contact details (email and phone) are visible to other parents, across every school at once. full makes them visible and name_only hides them. Home-address visibility is a separate control: see PATCH /me/address-visibility.
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
profile_visibility | string, one of "full", "name_only" | Yes |
Responses
204
Updated
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.
PATCH/me/address-visibility
Set the caller's own home-address visibility
Sets whether the caller's family home address is visible to other parents. It applies to every student the caller is a linked guardian of. Admins and the family's own linked guardians always see the address. A caller with no linked students gets a success with no effect.
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
address_visible | boolean | Yes |
Responses
204
Updated
400
address_visible must be a boolean
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.
POST/me/leave-school/{membershipId}
Leave a school immediately (no grace period)
Self-service, any role. Blocked with 409 if the caller is the last Senior Admin of that school (a school can never be left without one). Deletes the caller's own notes, stars, guardian links and list memberships for that school and removes the membership immediately, with no grace period.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
membershipId | path | string (uuid) | Yes |
Responses
204
Left
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.409
application/jsonErrorCaller is the school's only senior_admin
Photos
Student, parent, staff and organization photos, uploaded and downloaded through short-lived signed URLs.
POST/students/{studentId}/photo/upload-url
Get a pre-signed upload URL for a student's photo
The returned photo_key is deterministic (student-photos/{studentId}.{ext}) — re-uploading overwrites the previous photo rather than orphaning it, matching the existing convention. Max 5 MB, image/jpeg, image/png, image/webp, or image/gif only (enforced both by the upload-URL's constraints and re-validated when the client calls the confirm step below).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
studentId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
content_type | string, one of "image/jpeg", "image/png", "image/webp", "image/gif" | Yes |
Responses
200
application/jsonPhotoUploadUrlOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
POST/students/{studentId}/photo
Confirm a completed photo upload
Call after successfully PUT-ing bytes to the URL from upload-url above.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
studentId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
photo_key | string | Yes |
Responses
200
OK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
DELETE/students/{studentId}/photo
Remove a student's photo
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
studentId | path | string (uuid) | Yes |
Responses
204
Removed
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
GET/students/{studentId}/photo/download-url
Get a short-lived signed URL to view a student's photo
Clients must not cache or share this URL, and should re-request it each time a photo needs to be displayed rather than persisting it.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
studentId | path | string (uuid) | Yes |
Responses
200
application/jsonPhotoDownloadUrlOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorStudent has no photo
GET/parents/{parentId}/photo/download-url
Get a short-lived signed URL to view a parent/guardian's photo
Same 5-minute TTL signed GET URL shape as a student's own photo.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
parentId | path | string (uuid) | Yes |
Responses
200
application/jsonPhotoDownloadUrlOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorParent has no photo
POST/parents/{parentId}/photo
Confirm a completed parent photo upload
Stores photo_key on the parent's invitation row (an admin writes that one row; a parent uploading their own photo propagates it to every row with their email at the school, since a photo belongs to the person). photo_key must be the key issued for THIS parent. The superseded object is deleted once no other row references it. Allowed for an admin of the row's school, or the parent themselves (matched by sign-in email).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
parentId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
photo_key | string | Yes |
Responses
200
application/jsonobjectStored.
Field Type Required Description photo_keystring Yes 400
application/jsonErrorValidation failed
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
500
application/jsonErrorResponse
DELETE/parents/{parentId}/photo
Remove a parent's photo
Deletes the object from storage and clears photo_key (same admin-versus-self propagation as the confirm route). Idempotent, so 204 even when there is no photo.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
parentId | path | string (uuid) | Yes |
Responses
204
Removed.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
POST/parents/{parentId}/photo/upload-url
Get a pre-signed upload URL for a parent's photo
Same flow as the student photo upload-url. Allowed for an admin of the row's school or the parent themselves. Maximum size is enforced server side (413 above it).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
parentId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
content_type | string, one of "image/jpeg", "image/png", "image/webp", "image/gif" | Yes | |
content_length | integer | Yes | Size of the file in bytes. |
Responses
200
application/jsonPhotoUploadUrlOK
400
application/jsonErrorValidation failed
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
413
application/jsonErrorToo large
500
application/jsonErrorResponse
GET/staff/{staffId}/photo/download-url
Get a short-lived signed URL to view a staff member's photo
Open to any current member of the staff member's school. 5-minute lifetime; request it each time rather than storing it.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
staffId | path | string (uuid) | Yes |
Responses
200
application/jsonPhotoDownloadUrlOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
404
application/jsonErrorNot found
Menus
Lunch, breakfast and snack menu reads.
GET/schools/{schoolId}/menus
List every menu entry for the school
Every menu entry for the school across all dates, for caching. The client filters to the visible week or day locally. 404s if the school has the lunch_menus feature toggle turned off.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description menusarray of MenuEntryYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorThe
lunch_menusfeature is toggled off for this school.
GET/schools/{schoolId}/menus/week
Get one week's menus
start must be a Sunday (ISO date); the response covers that date through six days later inclusive. 404s if the school has the lunch_menus feature toggle turned off.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
start | query | string (date) | Yes | ISO date, must fall on a Sunday. |
Responses
200
application/jsonobjectOK
Field Type Required Description menusarray of MenuEntryYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorThe
lunch_menusfeature is toggled off for this school.
GET/schools/{schoolId}/menus/day
Get one day's menus (all meal types)
404s if the school has the lunch_menus feature toggle turned off.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
date | query | string (date) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description menusarray of MenuEntryYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorThe
lunch_menusfeature is toggled off for this school.
POST/schools/{schoolId}/menus/{menuId}/image
Get (lazily generating if needed) or force-regenerate a lunch entry's AI image
Two callers hit this same route: the plain (no force) path is open to any authenticated member of the school (parent or admin) — it generates only when there's no image yet, or the entry's current items text no longer matches the hash the last generation used, otherwise it returns the existing image_url immediately with no regeneration. ?force=true always regenerates regardless of current state and is restricted to an admin (junior or senior) — this is the client's "Regenerate Image" button. Only valid for meal_type: lunch entries. 404s if the school has the lunch_menus feature toggle turned off.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
menuId | path | string (uuid) | Yes | |
force | query | boolean | No | Always regenerate, even if an image already exists and its hash still matches. Admin only. |
Responses
200
application/jsonobjectOK — the (possibly freshly-generated, possibly already-existing) image.
Field Type Required Description image_urlstring (uri) Yes image_generated_atstring (date-time), nullable Yes 400
application/jsonErrorThe entry's
meal_typeis notlunch.401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot a member of this school, or
force=truewas used by a non-admin.404
application/jsonErrorMenu entry not found, or the
lunch_menusfeature is toggled off for this school.502
application/jsonErrorImage generation failed (provider error, rate limit, or content-safety rejection). Never happens as a side effect of saving/importing a menu — this route is only ever called after a menu entry already exists.
Calendar
School and organization calendar events, repeating-event series and calendar subscriptions.
GET/schools/{schoolId}/calendar
List every calendar event for the school
Every calendar event for the school across all dates, for caching. The client filters to the visible day, week or month locally; there is no date-range parameter in this API version. 404s if the school has the calendar feature toggle turned off.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description eventsarray of CalendarEventYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorThe
calendarfeature is toggled off for this school.
POST/schools/{schoolId}/calendar/subscriptions
Create a standing.ics calendar subscription on the caller's behalf
Creates a standing .ics calendar subscription for the calling user, returning an unguessable token that a calendar app can poll at https://studentdirectory.app/api/calendar/subscribe/{token}/feed.ics. A set-and-forget alternative to inserting events directly. categories and tags are inclusion filters (empty means the full calendar); organizationId, if given, scopes the feed to that organization's events. 404s if the school has the calendar feature toggle turned off.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Request body
application/json, optional. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | |
categories | array of string | No | |
tags | array of string | No | |
organizationId | string | No |
Responses
201
application/jsonobjectCreated
Field Type Required Description subscriptionobject Yes 400
application/jsonErrororganizationId does not belong to this school.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorThe
calendarfeature is toggled off for this school.
GET/organizations/{organizationId}/calendar-series
List an organization's (or class's, or department's) repeating events (admin or that organization's Editor)
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description seriesarray of CalendarSeriesYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorOrganization not found or Calendar disabled
POST/organizations/{organizationId}/calendar-series
Create a repeating event on an organization's calendar (admin or that organization's Editor)
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Request body
application/json, required. Type: CalendarSeriesCreateBody
Responses
201
application/jsonobjectCreated
Field Type Required Description seriesCalendarSeriesYes generated_countinteger Yes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorOrganization not found or Calendar disabled
422
application/jsonErrorThe rule or template is invalid
POST/organizations/{organizationId}/calendar-series/preview
Preview a repeating event on an organization's calendar (no writes)
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Request body
application/json, required. Type: CalendarSeriesCreateBody
Responses
200
application/jsonCalendarSeriesPreviewOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorOrganization not found or Calendar disabled
422
application/jsonErrorThe rule or template is invalid
GET/calendar-series/{seriesId}
Get a repeating event series (rule, template, summary, counts, flagged instances)
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
seriesId | path | string (uuid) | Yes | |
include | query | string, one of "occurrences" | No |
|
Responses
200
application/jsonobjectOK
Field Type Required Description seriesCalendarSeriesYes occurrencesarray of object No 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorSeries not found
PATCH/calendar-series/{seriesId}
Edit a series for this event, this and following, or all events
scope: this edits one instance (it becomes edited); following splits the series at occurrence_date and the response is the NEW series; all re-diffs every instance. Hand-edited instances are kept (kept_edited_count); a deleted instance is never resurrected.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
seriesId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
scope | string, one of "this", "following", "all" | Yes | |
occurrence_date | string (date) | No | |
fields | object | No | |
rule | CalendarSeriesRule | No | |
skip_closures | boolean | No |
Responses
200
application/jsonobjectOK
Field Type Required Description seriesCalendarSeriesYes affected_countinteger Yes kept_edited_countinteger Yes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorSeries or occurrence not found
422
application/jsonErrorInvalid scope, rule or fields
DELETE/calendar-series/{seriesId}
Delete a series for this event, this and following, or all events
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
seriesId | path | string (uuid) | Yes | |
scope | query | string, one of "this", "following", "all" | Yes | |
occurrence_date | query | string (date) | No |
Responses
200
application/jsonobjectOK
Field Type Required Description deleted_countinteger Yes kept_edited_countinteger Yes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorSeries or occurrence not found
422
application/jsonErrorInvalid scope or date
POST/organizations/{organizationId}/calendar-events
Create an event on an organization's own calendar
Requires an admin or a live Organization Editor grant on that organization, and the school's calendar feature to be on. Runs the same create logic as the web's organization-scoped event form (validation, editor authorization, audit log).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | |
description | string | No | |
category | string | No | Defaults to |
tags | string | No | Comma-separated. |
start_date | string (date) | Yes | |
end_date | string (date) | No | |
start_time | string | No | HH:MM, 24-hour, school time zone. |
end_time | string | No | HH:MM, 24-hour, school time zone. |
location | string, nullable | No | Optional venue; empty or null for none. |
Responses
201
application/jsonobjectCreated.
Field Type Required Description idstring (uuid) Yes 400
application/jsonErrorValidation failed
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
404
application/jsonErrorNot found
PATCH/schools/{schoolId}/calendar-events/{eventId}
Edit a calendar event
A full-form re-submit (not a sparse patch), except location: omitted leaves it unchanged. Authorization is decided by the event itself: a school-wide event needs an admin, an organization event needs an admin or a live Organization Editor grant. Returns the updated event.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
eventId | path | string (uuid) | Yes |
Request body
application/json, required. Type: CalendarEventInput
Responses
200
application/jsonCalendarEventThe updated event (with its series summary fields when it belongs to a series).
400
application/jsonErrorValidation failed
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
404
application/jsonErrorNot found
DELETE/schools/{schoolId}/calendar-events/{eventId}
Delete a calendar event
A hard delete. Same per-event authorization as the edit route; if the caller is refused the event survives and the response is 403.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
eventId | path | string (uuid) | Yes |
Responses
204
Deleted.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
404
application/jsonErrorNot found
Staff Directory
The school staff listing.
GET/schools/{schoolId}/staff
List the school's staff directory
Every staff member of the school, visible to any school member. 404s if the school has the staff_directory feature toggle turned off.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description staffarray of StaffMemberYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorThe
staff_directoryfeature is toggled off for this school.
Document Library
Documents and attachments visible to the user, plus authoring for Organization Editors.
GET/schools/{schoolId}/documents
List Document Library entries visible to the caller
Every document currently visible to the caller, with the auto-populated "School Details" entry prepended first when present - matching the web /documents page exactly, for every caller (admin or parent alike; the unfiltered admin management view has no /v1 equivalent). 404s if the school has the document_library feature toggle turned off.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description documentsarray of DocumentYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorThe
document_libraryfeature is toggled off for this school.
POST/schools/{schoolId}/documents
Create a document
An admin, or an Organization Editor creating for an organization they hold a live grant on (organization_id; the grant is verified server side). Attachments are uploaded first through POST /schools/{schoolId}/attachments and passed back in attachments. 404 when the document_library feature is off.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Request body
application/json, required. Type: DocumentInput
Responses
201
application/jsonobjectCreated.
Field Type Required Description idstring (uuid) Yes 400
application/jsonErrorValidation failed
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
404
application/jsonErrorNot found
GET/schools/{schoolId}/documents/{documentId}/attachments/{attachmentId}/download-url
Get a short-lived signed URL for one Document Library attachment
A DocumentAttachment.key is a B2 private-bucket object key, not a downloadable URL by itself - this route signs one, matching the existing student/parent/organization-photo download-url pattern (5-minute TTL, same PhotoDownloadUrl response shape). The synthetic "School Details" entry has no attachments and is never a valid documentId here.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
documentId | path | string | Yes | |
attachmentId | path | string (uuid) | Yes |
Responses
200
application/jsonPhotoDownloadUrlOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorThe
document_libraryfeature is toggled off, the document doesn't exist or isn't visible to the caller, or the attachment id doesn't belong to that document.
POST/schools/{schoolId}/attachments
Upload an attachment before its Document or Notice exists
The Bearer-token twin of the web's POST /api/attachments/upload: stores one file in the private bucket under the school's key prefix and returns its metadata (DocumentAttachment) WITHOUT tying it to any record. The client uploads each file as the user attaches it, then sends the whole attachments array with POST /schools/{schoolId}/documents (or the notices create), so the new document is created with its attachments and the audience's one-time "new document" alert names a complete document. Save rejects a key that is not this school's own.
Allowed for a school admin, or a user holding a live Organization Editor grant. Types: PDF, Word, Excel, JPEG, PNG. Maximum 10 MB.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Request body
multipart/form-data, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
file | string (binary) | Yes | |
entity_type | string, one of "document", "notice" | Yes | Selects the storage key prefix. |
Responses
201
application/jsonDocumentAttachmentUploaded.
400
application/jsonErrorValidation failed
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
413
application/jsonErrorToo large
500
application/jsonErrorResponse
GET/schools/{schoolId}/documents/{documentId}
Get one document
A single document's full detail, by id. The school-demographics sentinel id returns the auto-populated School Details entry. A document the caller may not see is a 404, the same as one that does not exist.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
documentId | path | string | Yes |
Responses
200
application/jsonDocumentOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
404
application/jsonErrorNot found
PATCH/schools/{schoolId}/documents/{documentId}
Edit a document
A full re-submit of the document. Authorization is decided by the document (an admin, or an Organization Editor with a live grant on the organization the document belongs to). Returns the updated document, or null if it could not be re-read.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
documentId | path | string | Yes |
Request body
application/json, required. Type: DocumentInput
Responses
200
application/jsonDocumentThe updated document.
400
application/jsonErrorValidation failed
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
404
application/jsonErrorNot found
DELETE/schools/{schoolId}/documents/{documentId}
Delete a document
A hard delete, including cleanup of the document's stored attachments. If the caller is refused the document survives and the response is 403.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
documentId | path | string | Yes |
Responses
204
Deleted.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
404
application/jsonErrorNot found
Notices
Time-bounded notices visible to the user, with read markers, plus authoring for Organization Editors.
GET/schools/{schoolId}/notices
List notices visible to the caller
Every notice currently visible to the caller, for admins and parents alike: audience targeting and expiry are already applied, so an expired notice or one addressed to other people is not returned. 404s if the school has the notices feature toggle turned off.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description noticesarray of NoticeYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorThe
noticesfeature is toggled off for this school.
POST/schools/{schoolId}/notices
Create a notice
An admin, or an Organization Editor creating for an organization they hold a live grant on (organization_id). Runs the same create logic as the web notice form (validation, audience, the one-time alert to the audience). 404 when the notices feature is off.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Request body
application/json, required. Type: NoticeInput
Responses
201
application/jsonobjectCreated.
Field Type Required Description idstring (uuid) Yes 400
application/jsonErrorValidation failed
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
404
application/jsonErrorNot found
POST/notices/{noticeId}/read
Mark a notice read
Marks a notice read for the caller. Private per user and idempotent (already-read is a successful no-op). The notice must be one the caller can see.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
noticeId | path | string (uuid) | Yes |
Responses
204
Marked read.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.500
application/jsonErrorResponse
DELETE/notices/{noticeId}/read
Mark a notice unread
Idempotent. Removes the caller's own read marker.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
noticeId | path | string (uuid) | Yes |
Responses
204
Marked unread (or it already was).
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.
GET/rich-text-images/resolve
Resolve an inline rich-text image reference to a signed URL
Turns the stable flux-image://<key> reference embedded in rich-text content into a 5-minute signed download URL. Keys outside the upload shape (for example path traversal into another folder) are refused.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
key | query | string | Yes | The image key from the |
Responses
200
application/jsonobjectOK
Field Type Required Description urlstring (uri) Yes expires_ininteger Yes Seconds until the URL expires (300).
400
application/jsonErrorValidation failed
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found
POST/rich-text-images/upload-url
Get a pre-signed upload URL for an inline rich-text image
For the native editor's image button. PUT the raw bytes to upload_url, then embed flux-image://<key> in the content. Allowed for a user who is an admin at any of their schools, or who holds at least one live Organization Editor grant. The gate is deliberately coarse, since the real authorization is at read time.
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
content_type | string, one of "image/jpeg", "image/png" | Yes | |
content_length | integer | Yes | Size in bytes. |
Responses
200
application/jsonobjectOK
Field Type Required Description upload_urlstring (uri) Yes keystring Yes expires_ininteger Yes 400
application/jsonErrorValidation failed
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
413
application/jsonErrorToo large
500
application/jsonErrorResponse
GET/schools/{schoolId}/notices/{noticeId}
Get one notice
One notice's full detail, by id, with is_read and audience_label. A notice the caller may not see answers 404, the same as one that does not exist.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
noticeId | path | string (uuid) | Yes |
Responses
200
application/jsonNoticeOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
404
application/jsonErrorNot found
PATCH/schools/{schoolId}/notices/{noticeId}
Edit a notice
Fields omitted keep their current value. An admin may change audience, category and flags; an Organization Editor edits through organization_id and cannot change the audience. Existing attachments are kept. Returns the updated notice, or null if it could not be re-read.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
noticeId | path | string (uuid) | Yes |
Request body
application/json, required. Type: NoticeInput
Responses
200
application/jsonNoticeThe updated notice.
400
application/jsonErrorValidation failed
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
404
application/jsonErrorNot found
DELETE/schools/{schoolId}/notices/{noticeId}
Delete a notice
A hard delete; the notice's read markers go with it. The caller needs the same rights as for an edit.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
noticeId | path | string (uuid) | Yes |
Responses
204
Deleted.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
404
application/jsonErrorNot found
GET/schools/{schoolId}/notices/{noticeId}/attachments/{attachmentId}/download-url
Get a short-lived signed URL for one notice attachment
Same shape and 5-minute lifetime as the document attachment route. The notice must be visible to the caller; an unknown attachment is a 404.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
noticeId | path | string (uuid) | Yes | |
attachmentId | path | string (uuid) | Yes |
Responses
200
application/jsonPhotoDownloadUrlOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
404
application/jsonErrorNot found
Permission Slips
Permission slips addressed to the user and their children, with answers and a printable PDF.
GET/schools/{schoolId}/permission-slips
List permission slips visible to the caller
Every permission slip that has at least one of the caller's own linked students eligible for it. A slip never auto-hides once its required_by date passes: completed and outstanding slips both stay in the list, sorted by required_by ascending (most urgent first). 404s if the school has the permission_slips feature toggle turned off.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description permission_slipsarray of PermissionSlipYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorThe
permission_slipsfeature is toggled off for this school.
POST/schools/{schoolId}/permission-slips
Create a permission slip
An admin, or an Organization Editor issuing for an organization they hold a live grant on (organization_id). Create only: permission slips cannot be edited or deleted through this API. 404 when the permission_slips feature is off.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | |
body_delta | object | No | Rich-text Delta document for the body. |
audience_id | string (uuid), nullable | No | |
organization_id | string (uuid) | No | Issue for this organization. An Organization Editor's only path. |
required_by | string (date) | Yes | The date the slip must be completed by. |
Responses
201
application/jsonobjectCreated.
Field Type Required Description idstring (uuid) Yes 400
application/jsonErrorValidation failed
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
404
application/jsonErrorNot found
PATCH/schools/{schoolId}/permission-slips/{slipId}
Update a permission slip (admin, or an Editor of the organization it belongs to)
A full re-submit of the slip (title, body_delta, and required_by are required). Authorization comes from the access token, never the body: an admin of the school may also set audience_id (or point the slip at an organization with organization_id); an Editor of an organization may edit only slips addressed to that organization's own audience, may not pass audience_id, and may name only the organization the slip already belongs to. 403 otherwise.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
slipId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
title | string | No | |
body_delta | object | No | Quill Delta document for the body. |
audience_id | string (uuid), nullable | No | Admin only. |
organization_id | string (uuid) | No | |
required_by | string (date) | No |
Responses
200
application/jsonobjectUpdated.
Field Type Required Description idstring (uuid) Yes 400
application/jsonErrorValidation failed (blank title, missing required-by date, body too long, invalid audience).
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
DELETE/schools/{schoolId}/permission-slips/{slipId}
Delete a permission slip (admin, or an Editor of the organization it belongs to)
Hard delete (its recorded responses go with it). Same authorization as PATCH: an admin, or an Editor of the organization whose own audience the slip is addressed to; 403 for anyone else (the route re-reads the row to detect a silent refusal), 404 if it does not exist.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
slipId | path | string (uuid) | Yes |
Responses
204
Deleted.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
GET/permission-slips/{slipId}
Get one permission slip
Not found and not-eligible both collapse to a 404 — a caller can't distinguish "doesn't exist" from "not permitted" (mirrors GET /students/{studentId}).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
slipId | path | string (uuid) | Yes |
Responses
200
application/jsonPermissionSlipOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
GET/permission-slips/{slipId}/pdf
Get a printable PDF export of the slip
A printable PDF of the slip. Returns the PDF bytes (Content-Type: application/pdf) rather than a redirect or signed URL. Available to the same callers as GET /permission-slips/{slipId}.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
slipId | path | string (uuid) | Yes | |
format | query | string, one of "letter", "a4" | No |
Responses
200
application/pdfstring (binary)The generated PDF.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
POST/permission-slips/{slipId}/respond
Answer the slip (Yes / No) for one or more linked students
Records the caller's answer per student: granted (Yes) or denied (No, with an optional reason). Two request shapes are accepted. Batch: { "answers": [{ "student_id", "decision", "reason"? }, ...] } sends every chosen student at once and is all-or-nothing: if any student is not the caller's own currently-targeted child, nothing is saved (403). Legacy single student: { "student_id" }, optionally with decision and reason; a missing decision is a grant and never overwrites an answer already on file. An answer can be changed (granted or denied, and the reason edited) any number of times until the slip's window closes, which is the day after required_by ends (UTC); after that the route returns 400 and the slip is read-only. Re-sending an unchanged answer is a harmless no-op. Moving back to granted clears the reason. reason is trimmed, kept only for a denial, and capped at 500 characters. The legacy response field completed_at is the first answer's timestamp.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
slipId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
answers | array of object | No | |
student_id | string (uuid) | No | Legacy single-student shape (use |
decision | string, one of "granted", "denied" | No | |
reason | string, nullable | No |
Responses
200
application/jsonobjectRecorded (or unchanged, returned idempotently).
Field Type Required Description completed_atstring (date-time) Yes answersarray of object Yes 400
application/jsonErrorInvalid answers (missing
student_id, unknowndecision, over-longreason, duplicate student, empty list), or the slip's window has closed.401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAt least one student is not the caller's own guardian-linked child currently targeted by this slip (nothing is saved).
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
Features
Per-school feature toggles, time zone and sidebar counts.
GET/schools/{schoolId}/features
Get the school's current feature toggle state
The school's current feature toggles as a map of feature key to boolean, so a client can hide or disable gated screens up front instead of discovering a disabled feature through a 404. Open to any school member.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description featuresFeatureTogglesand objectYes The
FeatureKey -> booleanmap, plus the EFFECTIVEclassesanddepartmentsflags: true only when the Operator made the feature available AND the school turned it on (opt-in at both levels, so absent/false means off - unlike the older keys, where a missing key means on).timezonestring No The school's IANA timezone, e.g.
America/New_York.401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
GET/schools/{schoolId}/timezone
Get the school's IANA timezone
Open to any school member.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description timezonestring Yes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
GET/schools/{schoolId}/sidebar-counts
Get the caller's sidebar "needs attention" counts for Polls, Permission Slips and Sign-Ups
Per-user counts behind the sidebar badges (zero means show no badge). Every caller, admins included, gets the same per-user semantics as a parent; only the caller's own state is reported. A feature turned off for the school counts 0 (this route does not 404 on a disabled feature). polls: visible polls that are open right now and that the caller has not answered. permission_slips: slips for which at least one of the caller's linked students is targeted and not yet completed, and whose required_by date has not passed (counted through the end of that day in the viewer's zone); a slip counts once however many of the caller's children still owe it. signups: visible sign-ups that are open for responses, that the caller did not create or administer as an Organization Editor, and that the caller has not yet opened (GET /signups/{signupId} records the open).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
tz | query | string | No | IANA time zone of the viewer (e.g. |
Responses
200
application/jsonobjectOK
Field Type Required Description pollsinteger Yes permission_slipsinteger Yes signupsinteger Yes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
Watch
A compact roster and star payload for companion watch apps.
GET/schools/{schoolId}/watch-payload
Get the compact roster + star payload for WatchConnectivity relay
A compact roster and star payload for companion watch apps: deliberately minimal fields (no address, no photo, no notes). Star changes made on the watch go back through POST and DELETE /students/{studentId}/star, called by the paired phone.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Responses
200
application/jsonWatchPayloadOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.
Organizations
Teams, performing arts, clubs and classes: description, leadership, roster, results, schedule and photos. Reads are open to any school member; writes need an Organization Editor grant.
GET/schools/{schoolId}/organizations
List the school's organizations
Non-archived only by default; includeArchived=true is honored for admins only. Optional category filter (athletics, performing_arts, clubs, other). 404s if the organizations feature toggle is off. kind selects what is listed: absent or organization returns only ordinary organizations; kind=class or kind=department returns { organizations: [ClassOrDepartment] } and 404s unless that feature is active for the school. A hidden class roster yields only a count and the caller's own linked children, and a private class is not returned to a parent unless their child is on it.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
kind | query | string, one of "organization", "class", "department" | No | organization (default) | class | department |
category | query | string, one of "athletics", "performing_arts", "clubs", "other" | No | |
includeArchived | query | boolean | No |
Responses
200
application/jsonobjectOK
Field Type Required Description organizationsarray of OrganizationSummaryYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorThe
organizationsfeature is toggled off for this school.
GET/schools/{schoolId}/organizations/{shortName}
Get an organization's full detail
Core fields, photo keys (not signed URLs - see the Photos routes below), leadership, roster, its filtered calendar events, and can_edit/can_manage for the caller. An archived organization 404s for a non-admin caller.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes | |
shortName | path | string | Yes |
Responses
200
application/jsonOrganizationOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
PATCH/organizations/{organizationId}
Update an organization's description (Admin or org editor)
A name field in this route's body is silently ignored.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Request body
application/json, optional. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
description_delta | object | No | Quill Delta document for the description. |
Responses
200
application/jsonOrganizationSummaryOK
400
application/jsonErrorValidation failed: a field is missing or invalid.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot an admin or a live org-editor grant for this organization.
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
GET/organizations/{organizationId}/schedule
A class's weekly meeting patterns
Anyone who can see the class. Each pattern is a weekly repeating series of source class_schedule whose events appear on the class calendar. 404 unless the organization is a class and Classes is active.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description patternsarray of ClassSchedulePatternYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot a class, or Classes disabled
POST/organizations/{organizationId}/schedule
Add a meeting pattern to a class (Admin or the class's Editor)
Generates the pattern's events on the class calendar (skipping school closures unless skip_closures is false). Weekly only; the date range is normally the class's term. At most 400 meetings.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Request body
application/json, required. Type: ClassSchedulePatternBody
Responses
201
application/jsonobjectCreated
Field Type Required Description patternClassSchedulePatternNo generated_countinteger No 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot a class, or Classes disabled
422
application/jsonErrorThe pattern is invalid
POST/organizations/{organizationId}/schedule/preview
Preview the dates a meeting pattern would generate (no writes)
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Request body
application/json, required. Type: ClassSchedulePatternBody
Responses
200
application/jsonCalendarSeriesPreviewOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot a class, or Classes disabled
422
application/jsonErrorThe pattern is invalid
PATCH/organizations/{organizationId}/schedule/{seriesId}
Replace one meeting pattern (regenerates only its own events)
Same body as create. Events someone edited by hand are kept; events added by hand are never touched.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes | |
seriesId | path | string (uuid) | Yes |
Request body
application/json, required. Type: ClassSchedulePatternBody
Responses
200
application/jsonobjectOK
Field Type Required Description patternClassSchedulePatternNo affected_countinteger No kept_edited_countinteger No 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found
422
application/jsonErrorThe pattern is invalid
DELETE/organizations/{organizationId}/schedule/{seriesId}
Remove a meeting pattern and the events it generated
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes | |
seriesId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description deleted_countinteger No 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found
GET/organizations/{organizationId}/end-of-term
A class's term dates, end-of-term plan and latest runs (Admin or the class's Editor)
plan is null when none is saved, and then nothing happens at the end of the term. The plan runs once per term, shortly after ends_on has finished in the SCHOOL's timezone.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description organization_idstring (uuid) No starts_onstring (date), nullable No ends_onstring (date), nullable No lifecyclestring, one of "active", "awaiting_dates", "archived" No planEndOfTermPlanNo runsarray of EndOfTermRunNo 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot a class, or Classes disabled
PUT/organizations/{organizationId}/end-of-term
Save (or remove) the end-of-term plan
Body is an object with a plan key holding an EndOfTermPlan, or plan set to null to remove it. Every key of the plan is required; anything left at keep is untouched at the end of the term.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
plan | EndOfTermPlan | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description planEndOfTermPlanNo 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot a class, or Classes disabled
422
application/jsonErrorThe plan is invalid
POST/organizations/{organizationId}/end-of-term/preview
Preview what running the saved plan now would change (no writes)
422 when no plan is saved or the class is archived.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Responses
200
application/jsonEndOfTermResultOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot a class, or Classes disabled
422
application/jsonErrorNo plan saved, or the class is archived
POST/organizations/{organizationId}/end-of-term/run
Run the saved end-of-term plan now ("Run reset now")
Runs the plan immediately. Idempotent per term (a second call answers already_ran: true), recorded in the school audit log and the class's run history, and the class's Editors are notified. Not undoable.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Responses
200
application/jsonEndOfTermResultOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot a class, or Classes disabled
422
application/jsonErrorNo plan saved, or the class is archived
POST/organizations/{organizationId}/leaders
Add a leadership entry (Admin or org editor)
Either school_staff_id (validated against the organization's own school) or a freeform display_name/display_title. display_title doubles as the org-specific role label (e.g. "Head Coach") for both cases.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Request body
application/json, optional. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
school_staff_id | string (uuid), nullable | No | |
display_name | string, nullable | No | |
display_title | string, nullable | No | |
display_order | integer | No |
Responses
201
application/jsonOrganizationLeaderCreated
400
application/jsonErrorValidation failed: a field is missing or invalid.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
PATCH/organizations/{organizationId}/leaders/{leaderId}
Edit a leadership entry (Admin or org editor)
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes | |
leaderId | path | string (uuid) | Yes |
Request body
application/json, optional. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
school_staff_id | string (uuid), nullable | No | |
display_name | string, nullable | No | |
display_title | string, nullable | No | |
display_order | integer | No |
Responses
200
application/jsonOrganizationLeaderOK
400
application/jsonErrorValidation failed: a field is missing or invalid.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
DELETE/organizations/{organizationId}/leaders/{leaderId}
Remove a leadership entry (Admin or org editor)
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes | |
leaderId | path | string (uuid) | Yes |
Responses
204
No Content
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
GET/organizations/{organizationId}/members
List an organization's parent members
Roster-visible content - any school member can read it, not just admin/editor, matching the leaders/students routes' own visibility.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Responses
200
application/jsonarray ofOrganizationParentMemberOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
POST/organizations/{organizationId}/members
Add a parent as an organization member
Independent of whether that parent's child is on the org's roster. For a Self-Sign-Up organization's own self-service join (no Editor/Admin grant required), see POST /organizations/{organizationId}/members/self-join instead.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
membership_id | string (uuid) | Yes |
Responses
201
application/jsonOrganizationParentMemberCreated
400
application/jsonErrorValidation failed: a field is missing or invalid.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
DELETE/organizations/{organizationId}/members/{membershipId}
Remove an organization member
Self-removal ("leave") is out of scope for this build - a self-joined member is removed the same way any member is, by an Editor or Admin.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes | |
membershipId | path | string (uuid) | Yes |
Responses
204
No Content
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
POST/organizations/{organizationId}/members/self-join
Self-join a Self-Sign-Up organization
Any parent at the school can join without Editor or Admin approval, but only when the organization's membership_mode is self_signup (fixed when the organization is created). The membership used is always the caller's own at the organization's school, never taken from the request body. Idempotent: joining an organization the caller already belongs to returns 204, not an error.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Responses
201
application/jsonOrganizationParentMemberCreated
204
No Content (already a member)
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
GET/organizations/{organizationId}/candidates
Minimal candidate list for the add-member/add-editor picker (Admin or org editor)
Excludes deactivated memberships; returns only id/email/role.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Responses
200
application/jsonarray ofOrganizationCandidateOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
GET/organizations/{organizationId}/message-recipients
Resolved "Message Everyone" recipient list, including phone (Admin or org editor)
The resolved "Message Everyone" recipient list for an organization, including phone numbers: every guardian of every student on the roster (email and phone masked by each guardian's privacy settings) plus every parent member, deduplicated by email. Requires an Admin or an Editor of the organization.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Responses
200
application/jsonarray ofMessageRecipientOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
GET/organizations/{organizationId}/results
List an organization's results
Newest first, then sort_order. 404 when the organization's results feature is off - the same answer whether or not rows exist.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description resultsarray of OrganizationResultYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
POST/organizations/{organizationId}/results
Add a result (Admin or org editor)
At least one of title/competitor is required. calendar_event_id is optional and must be one of THIS organization's events. No link to any student.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Request body
application/json, required. Type: OrganizationResultInput
Responses
201
application/jsonobjectCreated
Field Type Required Description idstring (uuid) Yes 400
application/jsonErrorValidation failed: a field is missing or invalid.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
PATCH/organizations/{organizationId}/results/{resultId}
Edit a result (Admin or org editor)
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes | |
resultId | path | string (uuid) | Yes |
Request body
application/json, required. Type: OrganizationResultInput
Responses
200
application/jsonOrganizationResultThe updated result
400
application/jsonErrorValidation failed: a field is missing or invalid.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
DELETE/organizations/{organizationId}/results/{resultId}
Delete a result (Admin or org editor)
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes | |
resultId | path | string (uuid) | Yes |
Responses
204
Deleted
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
POST/organizations/{organizationId}/students
Add a student to the roster
Manual add only - no CSV bulk import. student_id is validated against the organization's own school.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
student_id | string (uuid) | Yes |
Responses
201
application/jsonOrganizationRosterStudentCreated
400
application/jsonErrorValidation failed: a field is missing or invalid.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
DELETE/organizations/{organizationId}/students/{studentId}
Remove a student from the roster (Admin or org editor)
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes | |
studentId | path | string (uuid) | Yes |
Responses
204
No Content
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
POST/organizations/{organizationId}/photos/{slot}/upload-url
Get a presigned direct-to-B2 upload URL for one photo slot (Admin or org editor)
Same pre-signed-PUT-URL pattern as the student-photo routes. slot is 1, 2, or 3.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes | |
slot | path | string, one of "1", "2", "3" | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
content_type | string, one of "image/jpeg", "image/png", "image/webp", "image/gif" | Yes | |
content_length | integer | Yes | Bytes, max ~2.2 MB. |
Responses
200
application/jsonobjectOK
Field Type Required Description upload_urlstring Yes photo_keystring Yes expires_ininteger Yes 400
application/jsonErrorValidation failed: a field is missing or invalid.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
413
application/jsonErrorDeclared content_length exceeds the server max.
GET/organizations/{organizationId}/photos/{slot}/download-url
Get a short-lived signed download URL for one photo slot
Open to any school member who can see the organization.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes | |
slot | path | string, one of "1", "2", "3" | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description download_urlstring Yes expires_ininteger Yes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
POST/organizations/{organizationId}/photos/{slot}
Confirm a photo upload (Admin or org editor)
Called after the client PUTs bytes to the upload-url above, to store the resulting key.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes | |
slot | path | string, one of "1", "2", "3" | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
photo_key | string | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description photo_keystring Yes 400
application/jsonErrorValidation failed: a field is missing or invalid.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
DELETE/organizations/{organizationId}/photos/{slot}
Remove a photo (Admin or org editor)
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
organizationId | path | string (uuid) | Yes | |
slot | path | string, one of "1", "2", "3" | Yes |
Responses
204
No Content
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
Notifications
Resolve a tapped link to the right school and mark a notification read.
GET/deep-links/resolve
Which of the caller's schools does a tapped link belong to
A tapped Universal Link or push only carries a path such as /notices/{id} — nothing in it names a school. The app calls this before navigating: if the answer is a different school from the one selected, it switches first; on 404 it opens quietly and goes nowhere (no "not found" screen). A school is returned only when opening the item there would succeed for this caller — same membership, feature-toggle, audience and expiry rules as the item's own detail route. Recognized paths: /notices/{uuid}, /polls/{uuid}, /permission-slips/{uuid}, /documents/{uuid}, /signups/{uuid}, /organizations/{shortName}. An organization's short name is unique only within a school: active_school_id wins when that school has one by that name, otherwise the caller's only school that does (two candidates and no preference is a 404).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
path | query | string | Yes | The link's path, e.g. |
active_school_id | query | string (uuid) | No | The school currently selected in the app. |
Responses
200
application/jsonobjectThe school to open the item in
Field Type Required Description school_idstring (uuid) Yes 400
application/jsonErrorValidation failed: a field is missing or invalid.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
POST/notifications/{notificationId}/read
Mark one of the caller's own notifications read
Marks one of the caller's own notifications read. One-directional (it is never marked unread again) and idempotent.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
notificationId | path | string (uuid) | Yes |
Responses
204
Marked read
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.
Polls
Polls visible to the user, with results where the poll allows them, and answering.
GET/schools/{schoolId}/polls
List polls visible to the caller
Every poll currently visible to the caller: audience targeting and the rule that a poll is hidden until it opens are already applied. closed and already_responded are computed per poll so a client can badge it without its own schedule logic. 404s if the school has the polls feature toggle turned off.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description pollsarray of PollYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorThe
pollsfeature is toggled off for this school.
GET/polls/{pollId}
Get one poll's full detail, including questions/choices and (when visible) results
One poll's full detail, including questions and choices. The route has no school in its path; the poll's school is resolved for you. A poll that does not exist and one the caller cannot see both answer 404. results holds the poll's current results whenever its results_visibility mode allows them right now, and is null otherwise (for example never, or after_close before the poll has closed); do not compute results client-side.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
pollId | path | string (uuid) | Yes |
Responses
200
application/jsonPollDetailOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
POST/polls/{pollId}/respond
Submit answers to a poll
Submits the caller's answers. Every question in the poll must be answered exactly once in the same request (no partial submission). One immutable response is kept per poll and caller: a duplicate submission is rejected, not overwritten. A caller who is not in the poll's audience gets not_authorized, unlike the GET routes, which answer 404 for a poll the caller cannot see.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
pollId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
answers | array of PollAnswerInput | Yes |
Responses
200
application/jsonobjectResponse recorded
Field Type Required Description submitted_atstring (date-time) Yes 400
application/jsonErrorThe poll has closed, the answer set fails validation (missing/duplicate/malformed answers, or an answer referencing a question/choice outside this poll), or the caller already responded (including a concurrent double-submit race).
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not a member of this poll's audience.
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
Sign-Ups
Volunteer and donation sign-ups visible to the user, and claiming, editing or withdrawing a response.
GET/schools/{schoolId}/signups
List sign-ups visible to the caller
Every sign-up currently visible to the caller: audience or organization-membership targeting, the rule that a sign-up is hidden until it opens, and the visible_until cutoff are already applied. closed and has_claims are computed per sign-up so a client can badge it without its own schedule logic. This is a summary view: no items and no claimant names (see GET /signups/{signupId}). 404s if the school has the signups feature toggle turned off.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
schoolId | path | string (uuid) | Yes |
Responses
200
application/jsonobjectOK
Field Type Required Description signupsarray of SignupYes 401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not authorized for this school/resource/role
404
application/jsonErrorThe
signupsfeature is toggled off for this school.
GET/signups/{signupId}
Get one sign-up's full detail, including every item's claimant list and remaining capacity
One sign-up's full detail. The route has no school in its path; the sign-up's school is resolved for you. A sign-up that does not exist and one the caller cannot see both answer 404. Items are split into time_slot_items (sorted chronologically) and other_items (in the order the organizer chose). Each item's claims array lists every current claimant's display name and note, remaining is its live capacity, and my_claim is the caller's own claim on that item, or null. Reading a sign-up marks it as opened by the caller, which clears it from their sidebar sign-up count.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
signupId | path | string (uuid) | Yes | |
prefetch | query | string, one of "1" | No | Pass |
Responses
200
application/jsonSignupDetailOK
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
PATCH/signups/{signupId}
Edit a sign-up
A full re-submit. Authorization is decided by the sign-up (an admin, or an Organization Editor on one of its organizations). Returns the saved sign-up with its items split into time-slot items (sorted by start) and other items (sorted by position).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
signupId | path | string (uuid) | Yes |
Request body
application/json, required. Type: SaveSignupBody
Responses
200
application/jsonobjectThe saved sign-up.
Field Type Required Description idstring (uuid) No titlestring No descriptionstring No Deprecated plain-text rendering of
description_delta, kept only for app builds 4 and earlier. Readdescription_delta; this field will be removed.description_deltaobject, nullable No audience_idstring (uuid), nullable No organization_idsarray of string (uuid) No opens_atstring (date-time), nullable No closes_atstring (date-time), nullable No visible_untilstring (date-time) No updated_atstring (date-time) No closedboolean No time_slot_itemsarray of SignupItemBodyNo other_itemsarray of SignupItemBodyNo 400
application/jsonErrorValidation failed
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
404
application/jsonErrorNot found
DELETE/signups/{signupId}
Delete a sign-up
A hard delete; its items, responses and organization links go with it. If the caller is refused the sign-up survives and the response is 403.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
signupId | path | string (uuid) | Yes |
Responses
204
Deleted.
401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorNot allowed
404
application/jsonErrorNot found
POST/signups/{signupId}/respond
Claim, edit, or withdraw a response to one of this sign-up's items
Claims, edits or withdraws the caller's response on one item of a sign-up, subject to the sign-up being open and the caller being in its audience (a caller outside the audience gets not_authorized, unlike the GET routes, which answer 404). action: "edit" with just item_id (and an optional note) updates the note on the caller's existing claim in place; adding to_item_id moves the claim to a different item of the same sign-up (the new claim is created before the old one is removed, so a full target item leaves the original claim intact). A to_item_id equal to item_id means no move. The response is intentionally minimal: item_id, claimed, note and remaining for the item the claim now lives on. Call GET /signups/{signupId} again for the full claimant list.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
signupId | path | string (uuid) | Yes |
Request body
application/json, required. Type: object
| Field | Type | Required | Description |
|---|---|---|---|
action | string, one of "claim", "edit", "withdraw" | Yes | |
item_id | string (uuid) | Yes | The item being claimed/edited/withdrawn - or, for |
to_item_id | string (uuid) | No |
|
note | string, nullable | No | Optional free-text note. Ignored for |
Responses
200
application/jsonobjectClaimed, edited, or withdrawn successfully.
Field Type Required Description item_idstring (uuid) Yes The item the caller's claim now lives on (the target item, for a change) - or the item just withdrawn from, for
withdraw.claimedboolean Yes False only after a successful withdraw.
notestring, nullable Yes remaininginteger, nullable Yes item_id's current remaining capacity, recomputed after this write. Null for an uncapped item.
400
application/jsonErrorMissing/invalid
action/item_id, the sign-up has closed,to_item_iddoesn't belong to the same sign-up asitem_id, the target item is full, the caller already claimed the target item (a concurrent double-claim race), or the caller had no existing claim to edit/withdraw.401
application/jsonErrorThe access token is missing, expired or invalid (
not_authenticated). Also returned with the codemfa_requiredwhen the user has enrolled a second factor but the token has not completed it: show a two-factor code prompt, verify, and retry with the upgraded token.403
application/jsonErrorAuthenticated, but not a member of this sign-up's resolved audience.
404
application/jsonErrorNot found, OR exists but the caller has no access to it — this API deliberately does not distinguish the two (returning 403 instead of 404 for an existing-but-forbidden resource would confirm the resource's existence to a caller who shouldn't know that).
Schemas
Objects named in the request and response tables above.
CalendarSeriesRule
| Field | Type | Required | Description |
|---|---|---|---|
frequency | string, one of "daily", "weekly", "monthly" | Yes | |
interval | integer | No | |
weekdays | array of integer, nullable | No | weekly only; defaults to the start date's weekday |
monthly_mode | string, one of "day_of_month", "nth_weekday", nullable | No | |
day_of_month | integer, nullable | No | |
nth | integer, nullable | No | 1 to 4, or -1 for the last |
weekday | integer, nullable | No | |
starts_on | string (date) | Yes | |
ends_on | string (date), nullable | No | |
count | integer, nullable | No |
CalendarSeriesTemplate
| Field | Type | Required | Description |
|---|---|---|---|
title | string | No | |
description | string | No | |
category | string, one of "general", "performance", "closure", "athletic", "other" | No | |
tags | string | No | Comma-separated |
start_date | string (date) | No | |
end_date | string (date) | No | |
start_time | string | No | HH:MM or empty for all day |
end_time | string | No | |
location | string | No |
CalendarSeriesCreateBody
| Field | Type | Required | Description |
|---|---|---|---|
template | CalendarSeriesTemplate | Yes | |
rule | CalendarSeriesRule | Yes | |
skip_closures | boolean | No | |
event_id | string (uuid), nullable | No |
ClassSchedulePatternBody
| Field | Type | Required | Description |
|---|---|---|---|
title | string | No | Defaults to the class name |
weekdays | array of integer | Yes | Sunday is 0 |
interval | integer | No | Every N weeks (default 1) |
start_time | string, nullable | No | |
end_time | string, nullable | No | |
location | string, nullable | No | Defaults to the class location |
starts_on | string (date) | Yes | |
ends_on | string (date) | Yes | |
skip_closures | boolean | No |
ClassSchedulePattern
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | No | The series id; use it with PATCH/DELETE |
summary | string | No | |
weekdays | array of integer | No | |
interval | integer | No | |
start_time | string | No | |
end_time | string | No | |
location | string | No | |
title | string | No | |
starts_on | string (date) | No | |
ends_on | string (date) | No | |
skip_closures | boolean | No | |
instance_count | integer | No | |
edited_count | integer | No |
EndOfTermPlan
| Field | Type | Required | Description |
|---|---|---|---|
outcome | string, one of "archive", "reset_and_continue" | Yes | |
roster | string, one of "keep", "clear" | Yes | |
schedule_events | string, one of "keep", "delete" | Yes | |
notices | string, one of "keep", "archive" | Yes | |
polls_signups | string, one of "keep", "archive" | Yes | |
documents | string, one of "keep", "archive", "delete" | Yes | Default keep |
staff | string, one of "keep", "clear" | Yes | Editor grants are never cleared |
EndOfTermCounts
| Field | Type | Required | Description |
|---|---|---|---|
roster_cleared | integer | No | |
schedule_events_deleted | integer | No | |
schedule_series | integer | No | |
notices_archived | integer | No | |
polls_archived | integer | No | |
signups_archived | integer | No | |
documents_changed | integer | No | |
staff_cleared | integer | No |
EndOfTermResult
| Field | Type | Required | Description |
|---|---|---|---|
already_ran | boolean | No | |
dry_run | boolean | No | |
outcome | string, one of "archive", "reset_and_continue" | No | |
counts | EndOfTermCounts | No | |
next_starts_on | string (date), nullable | No | |
next_ends_on | string (date), nullable | No | |
lifecycle_after | string, one of "active", "awaiting_dates", "archived" | No | |
awaiting_dates | boolean | No |
EndOfTermRun
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | No | |
ran_at | string (date-time) | No | |
trigger | string, one of "scheduled", "manual" | No | |
outcome | string, one of "archive", "reset_and_continue" | No | |
term_starts_on | string (date), nullable | No | |
term_ends_on | string (date), nullable | No | |
next_starts_on | string (date), nullable | No | |
next_ends_on | string (date), nullable | No | |
lifecycle_after | string | No | |
counts | EndOfTermCounts | No |
CalendarSeriesPreview
| Field | Type | Required | Description |
|---|---|---|---|
dates | array of string (date) | Yes | |
count | integer | Yes | |
skipped_for_closures | array of string (date) | Yes | |
would_exceed_cap | boolean | Yes | |
cap | integer | Yes |
CalendarSeries
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
organization_id | string (uuid), nullable | No | |
rule | CalendarSeriesRule | Yes | |
template | CalendarSeriesTemplate | Yes | |
skip_closures | boolean | Yes | |
source | string, one of "manual", "class_schedule" | Yes | |
summary | string | Yes | |
instance_count | integer | Yes | |
edited_count | integer | Yes | |
first_date | string (date), nullable | No | Earliest start among the instances (null when none are left) |
last_date | string (date), nullable | No | Latest end among the instances |
anchor_event_id | string (uuid), nullable | No | The earliest instance; any instance opens the edit-all flow |
flagged | array of object | Yes |
NoticeInput
Body for creating or editing a notice. Attachments are not accepted through this route (a create starts with none; an edit keeps the existing ones).
| Field | Type | Required | Description |
|---|---|---|---|
title | string | No | |
body_delta | object | No | Rich-text Delta document (present when saved through the rich-text editor). Omitted on edit leaves it untouched. |
audience_id | string (uuid), nullable | No | |
organization_id | string (uuid) | No | Publish for this organization. An Organization Editor's only path; the server sets the audience. |
expires_at | string (date-time) | No | Required on create. Omitted on edit keeps the current value. |
category_id | string (uuid), nullable | No | Admins only. Omitted means uncategorized on create and unchanged on edit; null means uncategorized. |
flags | array of string, one of "urgent" | No | Admins only. Omitted on edit leaves them as they are and an empty array clears them. Values outside the allowed set are rejected. |
SignupItemBody
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid), nullable | No | Existing item id when editing; null for a new item. |
description | string | No | |
quantity | integer, nullable | No | |
starts_at | string (date-time), nullable | No | Set for a time-slot item. |
ends_at | string (date-time), nullable | No |
SaveSignupBody
| Field | Type | Required | Description |
|---|---|---|---|
title | string | No | |
description_delta | object | No | Rich-text Delta document for the description. |
audience_id | string (uuid), nullable | No | |
organization_ids | array of string (uuid) | No | Accepted on edit only. On create the sign-up is never organization-scoped. |
opens_at | string (date-time), nullable | No | |
closes_at | string (date-time), nullable | No | |
visible_until | string (date-time) | No | Required. |
items | array of SignupItemBody | No |
DocumentInput
Body for creating or editing a Document Library entry. An edit is a full re-submit; the synthetic school-demographics entry keeps its existing behaviour server side.
| Field | Type | Required | Description |
|---|---|---|---|
title | string | No | |
body_delta | object | No | Rich-text Delta document, present when saved through the rich-text editor. |
audience_id | string (uuid), nullable | No | Null = every school member. |
organization_id | string (uuid) | No | Publish for this organization, class or department. This is an Organization Editor's only path (the server sets the audience); the caller must hold a live grant. |
attachments | array of DocumentAttachment | No | |
category_id | string (uuid), nullable | No | Omitted means uncategorized on create and unchanged on edit; null means uncategorized. |
CalendarEventInput
Event fields shared by the calendar-event create routes and the edit route. Every field is optional in the body but title and start_date are required to pass validation.
| Field | Type | Required | Description |
|---|---|---|---|
title | string | No | |
description | string | No | |
category | string | No | Defaults to |
tags | string | No | Comma-separated. |
start_date | string (date) | No | |
end_date | string (date) | No | |
start_time | string | No | HH:MM, 24-hour, in the school's time zone. |
end_time | string | No | HH:MM, 24-hour, in the school's time zone. |
location | string, nullable | No | Optional venue. On create, empty or null means none. On edit, omitted leaves it unchanged and empty or null clears it. |
Error
Every error response uses this shape.
| Field | Type | Required | Description |
|---|---|---|---|
error | object | Yes |
MeResponse
| Field | Type | Required | Description |
|---|---|---|---|
user_id | string (uuid) | Yes | |
email | string (email) | Yes | |
is_fully_deactivated | boolean | Yes | True if every membership is currently in its post-deletion grace-period window. |
address_visible | boolean, nullable | No | The caller's own home-address visibility to other parents, aggregated over their linked students (true only when all are visible). |
memberships | array of Membership | Yes |
Membership
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
school_id | string (uuid) | Yes | |
school_name | string | Yes | |
school_logo_url | string, nullable | No | |
school_latitude | number (double), nullable | No | The school's geocoded latitude (schools.latitude), or |
school_longitude | number (double), nullable | No | The school's geocoded longitude (schools.longitude), or |
school_status | string, one of "active", "archived", "suspended" | No | The school's lifecycle status. Anything other than |
school_theme | Theme | No | The school's resolved theme, or |
role | string, one of "senior_admin", "junior_admin", "parent" | Yes | |
profile_visibility | string, one of "full", "name_only" | Yes |
|
deactivated_at | string (date-time), nullable | No |
RegistrationConfirmResult
| Field | Type | Required | Description |
|---|---|---|---|
schools | array of object | Yes | |
students | array of object | Yes |
Guardian
| Field | Type | Required | Description |
|---|---|---|---|
first_name | string | No | |
last_name | string | No | |
relation | string, nullable | No |
RosterStudent
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
first_name | string | Yes | |
last_name | string | Yes | |
graduation_year | integer, nullable | No | |
street | string, nullable | No | |
city | string, nullable | No | |
state | string, nullable | No | |
zip | string, nullable | No | |
photo_key | string, nullable | No | |
is_starred | boolean | Yes | |
is_linked | boolean | Yes | Whether the calling user is a linked guardian of this student. |
guardians | array of Guardian | No |
RosterStudentFull
Same base fields as RosterStudent plus latitude/longitude, the caller's own note, and full guardian contact.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
first_name | string | Yes | |
last_name | string | Yes | |
graduation_year | integer, nullable | No | |
street | string, nullable | No | |
city | string, nullable | No | |
state | string, nullable | No | |
zip | string, nullable | No | |
latitude | number (double), nullable | No | |
longitude | number (double), nullable | No | |
photo_key | string, nullable | No | |
is_starred | boolean | Yes | |
is_linked | boolean | Yes | Whether the calling user is a linked guardian of this student. |
note | Note | Yes | |
guardians | array of ParentContact | Yes |
Note
At most one per (student, author) - no id, since GET/PUT are addressed by student, not by note identity.
| Field | Type | Required | Description |
|---|---|---|---|
body | string | Yes | |
created_at | string (date-time) | Yes | |
updated_at | string (date-time) | Yes |
ParentContact
first_name/last_name/relation are never masked.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | No | |
first_name | string, nullable | No | |
last_name | string, nullable | No | |
relation | string, nullable | No | |
email | string (email), nullable | No | |
phone | string, nullable | No |
StudentDetail
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
first_name | string | Yes | |
last_name | string | Yes | |
graduation_year | integer, nullable | No | |
street | string, nullable | No | |
city | string, nullable | No | |
state | string, nullable | No | |
zip | string, nullable | No | |
latitude | number (double), nullable | No | |
longitude | number (double), nullable | No | |
photo_key | string, nullable | No | |
is_starred | boolean | Yes | |
is_linked | boolean | Yes | |
note | Note | Yes | |
parents | array of ParentContact | Yes |
MapMarker
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
first_name | string | Yes | |
last_name | string | Yes | |
graduation_year | integer, nullable | No | |
latitude | number (double) | Yes | |
longitude | number (double) | Yes |
StudentList
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
name | string | Yes | |
description | string, nullable | No | |
created_at | string (date-time) | Yes |
StudentListDetail
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
name | string | Yes | |
description | string, nullable | No | |
created_at | string (date-time) | Yes | |
students | array of object | Yes |
Theme
A school's or organization's resolved theme - the server derives it from 1-3 seed colors and has already enforced WCAG AA, so clients only apply it (no color math client-side). null where it appears means the standard look. Colors are #rrggbb; each token exists for both light and dark mode.
| Field | Type | Required | Description |
|---|---|---|---|
version | integer | Yes | |
seeds | array of string | Yes | |
tokens | object | Yes |
ThemeTokens
| Field | Type | Required | Description |
|---|---|---|---|
accent | string | Yes | Fill for primary buttons, the header and pills. |
accentHover | string | Yes | |
onAccent | string | Yes | Label color on an accent fill (black or white, always at least 4.5:1). |
accentText | string | Yes | The accent as text/links/tint on page surfaces. |
accentMuted | string | Yes | Subtle accent-tinted background. |
background | string | Yes | |
surface | string | Yes | |
border | string | Yes | |
headerBackground | string | Yes | |
headerText | string | Yes | |
highlight | string | No | Present only when the theme has a third seed. |
highlightText | string | No | |
onHighlight | string | No |
MyContact
| Field | Type | Required | Description |
|---|---|---|---|
first_name | string, nullable | No | |
last_name | string, nullable | No | |
phone | string, nullable | No |
MenuImportItem
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | |
description | string, nullable | No | Free text. Nudged (in the web admin form/CSV template, not enforced) toward a visually descriptive sentence — this is what gets fed to on-device |
category | string, nullable | No | |
allergens | string, nullable | No |
MenuEntry
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
date | string (date) | Yes | |
meal_type | string, one of "breakfast", "am_snack", "lunch", "pm_snack", "other" | Yes | |
items | array of MenuImportItem | Yes | |
notes | string, nullable | No | "vendor substitution due to weather closure". |
source | string, nullable | No | Free text, not a DB enum — e.g. |
recurrence_group_id | string (uuid), nullable | No | |
needs_review | boolean | Yes | True when a scraper found conflicting content awaiting an admin's keep/use decision — see pending_* fields. |
pending_source | string, nullable | No | |
pending_items | array of MenuImportItem, nullable | No | |
pending_raw_text | string, nullable | No | |
image_url | string (uri), nullable | No | Only ever populated for lunch rows; null until the first successful generation via |
image_generated_at | string (date-time), nullable | No | Updates on every (re)generation - the change-marker a client compares against its own cached copy, since |
CalendarEvent
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
title | string | Yes | |
description | string | Yes | |
category | string, one of "general", "performance", "closure", "athletic", "other" | Yes | |
tags | array of string | Yes | Free-form admin-entered tags (e.g. team/production names) for finer-grained filtering than category alone. |
start_date | string (date) | Yes | |
end_date | string (date) | Yes | |
start_time | string, nullable | No | HH:MM:SS. Null = no specific time / all day (e.g. a closure spanning start_date..end_date). |
end_time | string, nullable | No | |
location | string, nullable | No | Optional venue shown under the event title. Additive - older clients ignore it. Accepted (as |
StaffMember
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
school_id | string (uuid) | Yes | |
name | string | Yes | |
title | string, nullable | Yes | |
email | string (email), nullable | Yes | |
phone | string, nullable | Yes | |
location | string, nullable | Yes | |
created_at | string (date-time) | Yes | |
updated_at | string (date-time) | Yes |
DocumentAttachment
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
filename | string | Yes | |
content_type | string | Yes | |
size | integer | Yes | Bytes. |
key | string | Yes | Backblaze B2 private-bucket object key - not a downloadable URL by itself. |
Document
id and created_by are plain strings, not always a UUID: the synthetic "School Demographic Details" entry uses the sentinel id school-demographics and created_by: "system" rather than a real document's UUID/author id.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | Yes | A document UUID, or the |
school_id | string (uuid) | Yes | |
title | string | Yes | |
body | string | Yes | Deprecated plain-text rendering of |
body_delta | object | No | Quill Delta document: the rich-text body. |
audience_id | string (uuid), nullable | Yes | Null = visible to every school member. Non-null audiences are already resolved server-side - every Document in this response is already visible to the caller. |
attachments | array of DocumentAttachment | Yes | |
created_by | string | Yes | A user UUID, or "system" for the auto-populated demographics entry. |
created_at | string (date-time) | Yes | |
updated_at | string (date-time) | Yes | |
category | string, nullable | No | The category's NAME (null = uncategorized). Additive. |
category_id | string (uuid), nullable | No | A |
category_color | string, nullable | No | A palette key; null = derive a stable color from the name. |
Category
A per-school content category shared by Documents and Notices. Any member reads; only a Senior Admin writes.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
school_id | string (uuid) | Yes | |
name | string | Yes | Unique per school, case-insensitive. |
color | string, one of "blue", "purple", "orange", "green", "pink", "teal", "indigo", "red", "mint", "cyan", "brown", "yellow", nullable | Yes | Palette key. |
sort_order | integer | Yes | |
created_at | string (date-time) | Yes |
Notice
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
school_id | string (uuid) | Yes | |
title | string | Yes | |
body | string | Yes | Deprecated plain-text rendering of |
body_delta | object | No | Quill Delta document: the rich-text body. |
audience_id | string (uuid), nullable | Yes | Null = visible to every school member. Non-null audiences are already resolved server-side - every Notice in this response is already visible to the caller. |
expires_at | string (date-time) | Yes | Already-expired notices are excluded from this response entirely, not just flagged. |
created_by | string (uuid) | Yes | |
created_at | string (date-time) | Yes | |
updated_at | string (date-time) | Yes | |
category | string, nullable | No | The category's NAME (null = uncategorized). Additive. |
category_id | string (uuid), nullable | No | A |
category_color | string, nullable | No | A palette key; null = derive from the name. |
flags | array of string, one of "urgent" | No | Flexible notice flags, |
audience_label | object or object | No | Who the notice is for, as the apps show it. |
PermissionSlip
students is only the caller's own guardian-linked students eligible for this slip (currently audience-matched, or already answered) - never every student targeted by the slip.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
school_id | string (uuid) | Yes | |
title | string | Yes | |
body | string | Yes | Deprecated plain-text rendering of |
body_delta | object | No | Quill Delta document: the rich-text body. |
audience_id | string (uuid), nullable | Yes | Null = targeted at every active student at the school. |
required_by | string (date) | Yes | Informational due date - a plain calendar date, not a hide-after instant. Does not affect visibility; an outstanding or completed slip stays in the list past this date. |
created_by | string (uuid) | Yes | |
created_at | string (date-time) | Yes | |
updated_at | string (date-time) | Yes | |
students | array of object | Yes |
Poll
The list-route shape - also the base every field of PollDetail below extends.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
school_id | string (uuid) | Yes | |
title | string | Yes | |
description | string | Yes | Deprecated plain-text rendering of |
description_delta | object | No | Quill Delta document: the rich-text description. |
audience_id | string (uuid), nullable | Yes | Null = visible to every school member. Non-null audiences are already resolved server-side - every Poll in this response is already visible to the caller. |
results_visibility | string, one of "live", "after_close", "never" | Yes | The admin's own choice of when the audience can see results - "live" while voting is open, "after_close" only once closed (until results_visible_until), or "never". |
results_visible_until | string (date-time), nullable | Yes | Only meaningful when results_visibility is "after_close" - null for the other two modes. |
opens_at | string (date-time) | Yes | A poll isn't included in this list at all before it goes live - see the |
closes_at | string (date-time) | Yes | |
created_by | string (uuid) | Yes | |
created_at | string (date-time) | Yes | |
updated_at | string (date-time) | Yes | |
closed | boolean | Yes | True once closes_at has passed. |
already_responded | boolean | Yes | True if the caller has already submitted a response to this poll (one immutable response per caller). |
PollChoice
An admin-authored answer choice for a multiple_choice or multi_select PollQuestion. Never present for fill_in_blank questions.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
label | string | Yes | |
position | integer | Yes |
PollQuestion
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
question_type | string, one of "multiple_choice", "multi_select", "fill_in_blank" | Yes | |
prompt | string | Yes | |
position | integer | Yes | |
choices | array of PollChoice | Yes | Populated for multiple_choice/multi_select questions, in position order; always empty for fill_in_blank. |
min_selections | integer, nullable | No | multi_select only - null means 1 (at least one choice required). Always null for other question types. |
max_selections | integer, nullable | No | multi_select only - null means no cap beyond however many choices exist. Always null for other question types. |
PollQuestionResults
One question's aggregated results - never includes respondent identity, only aggregate counts/text.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
question_type | string, one of "multiple_choice", "multi_select", "fill_in_blank" | Yes | |
prompt | string | Yes | |
choice_tallies | array of object | Yes | multiple_choice/multi_select only - one entry per admin-authored choice, in choice position order, count 0 if nobody picked it. Empty for fill_in_blank. |
text_answers | array of string | Yes | fill_in_blank only - free-text answers in submission order. Empty for multiple_choice/multi_select. |
total_responses | integer | Yes |
PollDetail
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
school_id | string (uuid) | Yes | |
title | string | Yes | |
description | string | Yes | Deprecated plain-text rendering of |
description_delta | object | No | Quill Delta document: the rich-text description. |
audience_id | string (uuid), nullable | Yes | Null = visible to every school member. Non-null audiences are already resolved server-side - every Poll in this response is already visible to the caller. |
results_visibility | string, one of "live", "after_close", "never" | Yes | The admin's own choice of when the audience can see results - "live" while voting is open, "after_close" only once closed (until results_visible_until), or "never". |
results_visible_until | string (date-time), nullable | Yes | Only meaningful when results_visibility is "after_close" - null for the other two modes. |
opens_at | string (date-time) | Yes | A poll isn't included in this list at all before it goes live - see the |
closes_at | string (date-time) | Yes | |
created_by | string (uuid) | Yes | |
created_at | string (date-time) | Yes | |
updated_at | string (date-time) | Yes | |
closed | boolean | Yes | True once closes_at has passed. |
already_responded | boolean | Yes | True if the caller has already submitted a response to this poll (one immutable response per caller). |
questions | array of PollQuestion | Yes | |
results | array of PollQuestionResults, nullable | Yes | The poll's current results, or null when the poll's own results_visibility mode doesn't allow them to this caller right now - see the |
PollAnswerInput
Exactly one of choice_id/answer_text/choice_ids is populated, matching the question's own type - choice_id for multiple_choice, answer_text for fill_in_blank, choice_ids (a non-empty array, size within the question's min_selections/max_selections) for multi_select. Every question in the poll must appear exactly once in a respond request's answers array.
| Field | Type | Required | Description |
|---|---|---|---|
question_id | string (uuid) | Yes | |
choice_id | string (uuid), nullable | Yes | |
answer_text | string, nullable | Yes | |
choice_ids | array of string (uuid) | No | multi_select only - the set of chosen choice IDs. Omitted/null for other question types. |
Signup
The list-route shape - also the base every field of SignupDetail below extends. The web parent UI itself never surfaces this field either.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
school_id | string (uuid) | Yes | |
title | string | Yes | |
description | string | Yes | Deprecated plain-text rendering of |
description_delta | object | No | Quill Delta document: the rich-text description. |
opens_at | string (date-time) | Yes | A sign-up isn't included in this list at all before it goes live - see the |
closes_at | string (date-time) | Yes | No further claims, edits, or withdrawals are accepted once this passes. |
visible_until | string (date-time) | Yes | The sign-up is no longer shown to its audience at all once this passes, independent of closes_at. |
created_by | string (uuid) | Yes | |
created_at | string (date-time) | Yes | |
updated_at | string (date-time) | Yes | |
closed | boolean | Yes | True once closes_at has passed. |
has_claims | boolean | Yes | True if the caller currently holds at least one claim anywhere on this sign-up. |
SignupItemClaim
One current claimant on a SignupItem. Never includes the claimant's user id.
| Field | Type | Required | Description |
|---|---|---|---|
display_name | string | Yes | |
note | string, nullable | Yes |
SignupItem
One claimable item (or time slot) on a sign-up.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
description | string | Yes | |
quantity | integer, nullable | Yes | The claim cap for this item, or null for uncapped. |
starts_at | string (date-time), nullable | Yes | Set together with ends_at for a time-slot item; both null for a plain (non-timed) item. |
ends_at | string (date-time), nullable | Yes | |
remaining | integer, nullable | Yes | Live remaining capacity (quantity minus current claim count), recomputed fresh on every read. Null for an uncapped item, never negative. |
claims | array of SignupItemClaim | Yes | Every current claimant on this item, in claim order. |
my_claim | object, nullable | Yes | The caller's own claim on this item (their own note only - their own name isn't repeated back to them), or null if they haven't claimed it. |
SignupDetail
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
school_id | string (uuid) | Yes | |
title | string | Yes | |
description | string | Yes | Deprecated plain-text rendering of |
description_delta | object | No | Quill Delta document: the rich-text description. |
opens_at | string (date-time) | Yes | A sign-up isn't included in this list at all before it goes live - see the |
closes_at | string (date-time) | Yes | No further claims, edits, or withdrawals are accepted once this passes. |
visible_until | string (date-time) | Yes | The sign-up is no longer shown to its audience at all once this passes, independent of closes_at. |
created_by | string (uuid) | Yes | |
created_at | string (date-time) | Yes | |
updated_at | string (date-time) | Yes | |
closed | boolean | Yes | True once closes_at has passed. |
has_claims | boolean | Yes | True if the caller currently holds at least one claim anywhere on this sign-up. |
time_slot_items | array of SignupItem | Yes | Items with a starts_at/ends_at set, sorted chronologically. |
other_items | array of SignupItem | Yes | Items with no starts_at/ends_at, in admin-chosen order. |
FeatureToggles
One boolean per known FeatureKey — a key with no explicit false entry is always present here as true (missing from the school's stored config means enabled, not omitted from this map).
| Field | Type | Required | Description |
|---|---|---|---|
lunch_menus | boolean | Yes | |
document_library | boolean | Yes | |
directory_map | boolean | Yes | |
notices | boolean | Yes | |
staff_directory | boolean | Yes | |
calendar | boolean | Yes | |
polls | boolean | Yes | |
permission_slips | boolean | Yes | |
signups | boolean | Yes |
PhotoUploadUrl
| Field | Type | Required | Description |
|---|---|---|---|
upload_url | string (uri) | Yes | Pre-signed B2 PUT URL. PUT the raw image bytes here directly — do not route them through this API. |
photo_key | string | Yes | Deterministic key (student-photos/{studentId}.{ext}) — pass back unchanged to the confirm endpoint. |
expires_in | integer | Yes | Seconds until upload_url expires. |
PhotoDownloadUrl
| Field | Type | Required | Description |
|---|---|---|---|
download_url | string (uri) | Yes | |
expires_in | integer | Yes | Seconds until download_url expires. Currently 300 (5 minutes), matching the existing web signed-URL TTL. |
WatchPayload
| Field | Type | Required | Description |
|---|---|---|---|
school_id | string (uuid) | Yes | |
generated_at | string (date-time) | Yes | |
students | array of object | Yes |
OrganizationSummary
The list-route shape - core fields only, no leaders/roster/events/photos.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
school_id | string (uuid) | Yes | |
category | string, one of "athletics", "performing_arts", "clubs", "other" | Yes | |
name | string | Yes | |
short_name | string | Yes | Unique per school, not globally. |
description | string | Yes | Deprecated plain-text rendering of |
description_delta | object | No | Quill Delta document: the rich-text description. |
archived_at | string (date-time), nullable | Yes | |
created_at | string (date-time) | No | |
updated_at | string (date-time) | No | |
theme | object, nullable | No | List route only. The organization's own resolved theme, or null to inherit the school's. |
can_edit | boolean | No | List route only. Whether the caller may edit this organization (admin, or a live editor grant); drives Student Detail's organization pill controls. |
Organization
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
school_id | string (uuid) | Yes | |
category | string, one of "athletics", "performing_arts", "clubs", "other" | Yes | |
name | string | Yes | |
short_name | string | Yes | Unique per school, not globally. |
description | string | Yes | Deprecated plain-text rendering of |
description_delta | object | No | Quill Delta document: the rich-text description. |
archived_at | string (date-time), nullable | Yes | |
created_at | string (date-time) | No | |
updated_at | string (date-time) | No | |
theme | Theme | No | This organization's own resolved theme, or |
can_edit | boolean | Yes | Admin or a live org-editor grant - can edit description/photos/leadership/roster. |
photo_1_key | string, nullable | Yes | B2 key, not a URL - fetch a signed URL via GET.../photos/{slot}/download-url. |
photo_2_key | string, nullable | Yes | |
photo_3_key | string, nullable | Yes | |
leaders | array of OrganizationLeader | Yes | |
roster | array of OrganizationRosterStudent | Yes | |
events | array of CalendarEvent | Yes | Empty if the |
features | map of boolean | No | Per-organization opt-in flags that APPLY to this organization's category (today |
notices | array of object | Yes | The notices that target this organization (its own audience, or a composite audience that includes it) that the caller is in the audience for, newest first. Empty if the |
documents | array of object | No | The documents that target this organization (its own audience, or a composite audience that includes it) that the caller is in the audience for, plus - for an editor/admin - the organization's own. Empty if the |
permission_slips | array of object | No | The permission slips that target this organization that the caller (a guardian of an eligible student) can respond to, most urgent first, plus - for an editor/admin - the organization's own. Empty if the |
can_manage | boolean | Yes | Admin only - archive/re-enable, category/short_name, editor grants. |
OrganizationLeader
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
school_staff_id | string (uuid), nullable | Yes | |
name | string | Yes | |
title | string, nullable | Yes | The org-specific role label (e.g. Resolved from display_title, falling back to the linked staff record's own title only when display_title is unset. |
display_order | integer | Yes |
OrganizationResult
A team/organization-level result; never linked to a student.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
organization_id | string (uuid) | Yes | |
calendar_event_id | string (uuid), nullable | Yes | Optional link to one of this organization's events. Deleting the event keeps the result (it becomes standalone). |
result_date | string (date) | Yes | |
title | string | Yes | e.g. '200m Freestyle', 'Board 1'. |
competitor | string | Yes | |
outcome | string, one of "win", "loss", "tie", "no_contest", nullable | Yes | |
score_summary | string | Yes | Free text: '21-14', '1:04.32', '3rd of 12'. |
notes_delta | object | Yes | Quill Delta (rich-text notes). |
sort_order | integer | Yes | |
created_at | string (date-time) | No | |
updated_at | string (date-time) | No |
OrganizationResultInput
| Field | Type | Required | Description |
|---|---|---|---|
result_date | string (date) | Yes | |
calendar_event_id | string (uuid), nullable | No | |
title | string | No | |
competitor | string | No | |
outcome | string, one of "win", "loss", "tie", "no_contest", nullable | No | |
score_summary | string | No | |
notes_delta | object | No | |
sort_order | integer | No | Optional; assigned (appended within the event/day) when omitted. |
OrganizationRosterStudent
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
first_name | string | Yes | |
last_name | string | Yes | |
graduation_year | integer, nullable | Yes | |
photo_key | string, nullable | Yes |
OrganizationParentMember
A parent membership of an organization, independent of the student roster or leadership. added_by is null for a self-service join and the adding user's id for an Editor/Admin-added row.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
membership_id | string (uuid) | Yes | |
user_id | string (uuid), nullable | Yes | |
email | string, nullable | Yes | |
added_by | string (uuid), nullable | Yes | |
created_at | string (date-time) | Yes |
OrganizationCandidate
A minimal membership row for the add-member/add-editor picker.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
email | string, nullable | Yes | |
role | string | Yes |
MessageRecipient
A resolved "Message Everyone" candidate for an organization or list - deduplicated by email, with phone included. A recipient with neither email nor phone is never returned.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
name | string | Yes | |
email | string, nullable | Yes | |
phone | string, nullable | Yes |
ListGuardian
An individual parent/guardian added directly to a Custom List, not derived from a student being on the list.
| Field | Type | Required | Description |
|---|---|---|---|
id | string (uuid) | Yes | |
guardian_membership_id | string (uuid) | Yes | |
linked_student_id | string (uuid), nullable | Yes | |
user_id | string (uuid), nullable | Yes | |
email | string, nullable | Yes | |
added_by | string (uuid) | Yes | |
created_at | string (date-time) | Yes |
