Errors & rate limits
All errors return a consistent JSON envelope:
{
"error": "ERROR_CODE",
"message": "Human-readable description",
"detail": "...",
"status_code": 400,
"context": { "field": "..." }
}
Status codes
| Status | Common codes | Meaning |
|---|---|---|
| 400 | INVALID_REQUEST, NO_CHAPTERS | Bad input |
| 401 | MISSING_AUTH, INVALID_KEY | Auth failure |
| 403 | TEST_USAGE_LIMIT, SEAT_LIMIT_REACHED, PLAN_REQUIRED | Policy denied |
| 404 | NOT_FOUND | Resource not found |
| 422 | (Pydantic validation) | Request body / form field invalid |
| 429 | RATE_LIMIT | Tenant or user rate limit exceeded |
| 500 | INTERNAL_ERROR | Server bug — contact support with X-Request-Id |
| 502/503 | UPSTREAM_UNAVAILABLE | LLM or vector DB upstream issue — retry with backoff |
When you get rate-limited
Read Retry-After and back off. Don’t retry tighter than the value it returns. If you’re consistently hitting limits, upgrade your tier from the Dashboard.
When the LLM upstream fails
The API surfaces upstream failures as 502/503 with X-Request-Id. Retry with exponential backoff (start at 1s, cap at 30s). If a single course generation fails halfway, use POST /courses/{id}/resume — it only re-runs the missing chapters.