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

These are the error-code strings the API actually emits. If you need the precise codes a specific endpoint can return, read that operation’s responses in the API Reference; do not assume codes that aren’t listed here.

codeWhere
PERMISSION_DENIED403: registration closed; missing permission
VALIDATION_ERROR422: body validation failed
QUICKCODE_INVALID_OR_EXPIRED401: student quickCode invalid or expired
QUICKCODE_RATE_LIMIT401: too many quickCode attempts

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.
Last updated on