message and a details object for debugging (never secrets). Rate-limited responses include Retry-After.
Status codes
success
Handler succeeded. Body is JSON unless the route streams PDF, DOCX, CSV, or HTML.
client error
Invalid JSON, missing
stepKey, bad schoolYear, or a validation failure on create/update.auth
No session, inactive user, denied
jti after logout, or JWT failed getToken in the proxy.authz
Authenticated but not allowed: wrong level, Super Admin-only route, or form RBAC (not owner, not same-school principal, not assigned).
client error
Unknown
User, FormSubmission, or step number with no matching bank key.conflict
School already has a plan for that year; step save revision conflict; next-year settings already exist.
rate limit
Too many requests. Respect
Retry-After (seconds). In production, some limits fail closed if Redis is down.server error
Uncaught exception. Logged with
console.error; optionally SENTRY_DSN via src/lib/reportError.js. Do not send form answers to Sentry.Rate limits
Counters live in Redis asrl:* keys (60-second windows unless noted). The proxy applies the auth limit by client IP (x-forwarded-for). Application limits use session.user.id.
- 429 body
- Redis down
Retry-After: 60Hitting a 403 on
/api/forms/:id/editors while the form page itself loads usually means the route’s RBAC is stricter than GET /api/forms/:id. Principals need same-school level 4 (or matching principalEmail). Assistant Principals need assignment or share. See Troubleshooting.