Errors and limits
What failure looks like, and how to stay inside the boundaries.
ProposedNot yet callable
This documents a design under review. Endpoints, payloads and field names may change before release.
Error shape
Every error shares one shape.
{
"error": {
"type": "invalid_request",
"code": "session_closed",
"message": "Session ses_55ab20 closed at 2026-07-19T10:05:00Z.",
"param": "session_id"
}
}Branch on code, not on message. Messages are written for humans and will be reworded.
Status codes
| Code | Meaning |
|---|---|
| 400 | Malformed request — bad JSON, unknown field, wrong type. |
| 401 | Missing or unrecognised API key. |
| 403 | Valid key without the required scope. |
| 404 | No such record, session or student. |
| 409 | Conflict — usually a write against a closed session. |
| 422 | Well-formed but rejected, e.g. a status outside the enum. |
| 429 | Rate limited. Honour Retry-After. |
| 5xx | Our fault. Retry with backoff; writes are idempotent. |
Rate limits
- 120 requests per minute per key.
- Responses carry
X-RateLimit-RemainingandX-RateLimit-Reset. - Exceeding it returns
429withRetry-Afterin seconds. Back off; do not spin.
Working at scale
For a nightly full sync, prefer /sessions/{id}/roll over paging every record — one request per session instead of thousands, and it includes students with no record.
Better still, take the live feed from webhooks and use this API only for reconciliation and corrections. A busy institution that polls this API all day will spend most of its rate limit being told nothing changed.