Skip to Content

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

StatusWhen
401Not authenticated · bad credentials · expired access token · invalid/rotated refresh token · invalid/expired quickCode · login brute-force lockout (with Retry-After)
403Authenticated but lacking permission within your own org (e.g. a second register, wrong-scope)
404Not found or cross-organization (privacy: never confirms a foreign resource exists)
409Conflict: class has students on delete · teacher already assigned · quickCode collision
422Validation: school.countryorg.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.

codeHTTP statusWhen it fires
INVALID_CREDENTIALS401Wrong email or password on login
TOKEN_MISSING401No Authorization header on a protected route
TOKEN_INVALID401Malformed or unrecognized access token
TOKEN_EXPIRED401Access token has passed its 15-minute lifetime; call POST /api/auth/refresh
SESSION_REVOKED401Refresh token was rotated-away or explicitly revoked
BRUTE_FORCE_LOCKED401Too many failed login attempts; honor the Retry-After header before retrying
QUICKCODE_INVALID_OR_EXPIRED401Student quickCode is invalid or has expired
QUICKCODE_RATE_LIMIT401Too many quickCode attempts from the same source
PERMISSION_DENIED403Authenticated but the caller’s permission set does not cover the operation
DEMO_READ_ONLY403A mutating or provisioning call was made on a demo organization
CONTEXT_FLAG_BLOCKED403The selected context flag is on the blocked list; no free-text note path exists
NOT_FOUND404Resource does not exist in this organization (also used cross-org to avoid disclosing foreign IDs)
EMAIL_EXISTS409User creation attempted with an email that already exists
CONFLICT409General conflict: class has students on delete, quickCode collision, or other state clash
DO_NOT_DECIDE_YET200Decision-engine safe-stop: evidence is split or insufficient; see the safe-stop table below
VALIDATION_ERROR422Request body failed schema validation; details[] names each failing field
RATE_LIMITED429General rate limit exceeded (e.g. POST /api/logs/client); honor Retry-After
ENGINE_UNAVAILABLE503The diagnostic or scoring engine could not be reached
STATE_EXPIRED401OAuth state or single-use handoff code has expired or was already consumed
INTERNAL500Unexpected 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:

OutcomeWhereMeaning
do_not_decide_yetskill-status, profile, monitoringEvidence is split or insufficient; every downstream consumer must halt and wait. See The Decision Engine.
blocked_insufficient_evidencebundle recommendationNo bundle can be recommended yet; the response names the next data action to take.
no_eligible_itemtask deliveryNo stored item matches the request; reported as a 200 with served: null (fail-loud, never a silent substitute).
DATA_INCOMPLETE (PR2-00)profile resolutionA 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-After before retrying.
  • On an expired access token, call POST /api/auth/refresh; don’t re-login.
  • Rate-limit windows reset on a successful request.