Errors
The error envelope
Every error, at every status, uses one shape: the ApiError schema:
{
"code": "VALIDATION_ERROR",
"message": "Human-readable description.",
"details": [{ "field": "password", "issue": "too short" }],
"retryAfter": 60
}code: a stable machine-readable key (always present).message: human-readable text (always present).details[]: per-field validation issues ({ field, issue }), present on validation failures.retryAfter: seconds to wait, present on rate-limited responses.
HTTP statuses
| Status | When |
|---|---|
401 | Not authenticated · bad credentials · expired access token · invalid/rotated refresh token · invalid/expired quickCode · login brute-force lockout (with Retry-After) |
403 | Authenticated but lacking permission within your own org (e.g. a second register, wrong-scope) |
404 | Not found or cross-organization (privacy: never confirms a foreign resource exists) |
409 | Conflict: class has students on delete · teacher already assigned · quickCode collision |
422 | Validation: school.country ≠ org.country · grade outside 1-4 · bad slug · short password |
Login brute-force returns 401 + Retry-After, not 429. This API does not emit a bare 429
for the login limiter. Read the Retry-After header (and retryAfter in the body) and back off.
404 ≠ “doesn’t exist”. A real resource in another organization is reported as 404 by design.
A resource in your own org that your role can’t reach is 403. See
Core Concepts → Tenant isolation.
Error-code catalog
The key error codes an API consumer must handle are listed below. For the precise codes a specific
endpoint can return, read that operation’s responses in the API Reference.
code | HTTP status | When it fires |
|---|---|---|
INVALID_CREDENTIALS | 401 | Wrong email or password on login |
TOKEN_MISSING | 401 | No Authorization header on a protected route |
TOKEN_INVALID | 401 | Malformed or unrecognized access token |
TOKEN_EXPIRED | 401 | Access token has passed its 15-minute lifetime; call POST /api/auth/refresh |
SESSION_REVOKED | 401 | Refresh token was rotated-away or explicitly revoked |
BRUTE_FORCE_LOCKED | 401 | Too many failed login attempts; honor the Retry-After header before retrying |
QUICKCODE_INVALID_OR_EXPIRED | 401 | Student quickCode is invalid or has expired |
QUICKCODE_RATE_LIMIT | 401 | Too many quickCode attempts from the same source |
PERMISSION_DENIED | 403 | Authenticated but the caller’s permission set does not cover the operation |
DEMO_READ_ONLY | 403 | A mutating or provisioning call was made on a demo organization |
CONTEXT_FLAG_BLOCKED | 403 | The selected context flag is on the blocked list; no free-text note path exists |
NOT_FOUND | 404 | Resource does not exist in this organization (also used cross-org to avoid disclosing foreign IDs) |
EMAIL_EXISTS | 409 | User creation attempted with an email that already exists |
CONFLICT | 409 | General conflict: class has students on delete, quickCode collision, or other state clash |
DO_NOT_DECIDE_YET | 200 | Decision-engine safe-stop: evidence is split or insufficient; see the safe-stop table below |
VALIDATION_ERROR | 422 | Request body failed schema validation; details[] names each failing field |
RATE_LIMITED | 429 | General rate limit exceeded (e.g. POST /api/logs/client); honor Retry-After |
ENGINE_UNAVAILABLE | 503 | The diagnostic or scoring engine could not be reached |
STATE_EXPIRED | 401 | OAuth state or single-use handoff code has expired or was already consumed |
INTERNAL | 500 | Unexpected server error |
Decision-engine “safe-stop” responses (not errors)
Several RTI-engine endpoints return 200 with a non-committal outcome rather than an error when
the data isn’t sufficient to decide. These are deliberate safety stops (V-3), not failures. Read the
outcome field and surface a “needs more data” state instead of retrying:
| Outcome | Where | Meaning |
|---|---|---|
do_not_decide_yet | skill-status, profile, monitoring | Evidence is split or insufficient; every downstream consumer must halt and wait. See The Decision Engine. |
blocked_insufficient_evidence | bundle recommendation | No bundle can be recommended yet; the response names the next data action to take. |
no_eligible_item | task delivery | No stored item matches the request; reported as a 200 with served: null (fail-loud, never a silent substitute). |
DATA_INCOMPLETE (PR2-00) | profile resolution | A system-guard profile, not a real educational profile. The student is excluded from clustering and bulk activation. |
Context flags use a closed dictionary: selecting a blocked flag (or any free-text note about a student) is rejected: there is no free-text path on a student record (V-12). See Language Safety.
Retry guidance
- On a rate-limited response, honor
Retry-Afterbefore retrying. - On an expired access token, call
POST /api/auth/refresh; don’t re-login. - Rate-limit windows reset on a successful request.