The LearningStudioAI API uses standard HTTP status codes and a single
JSON response shape. Every error response carries a machine-readable
code for programmatic handling.
Response shape
All error responses are JSON with the same two fields:
{
"message": "Human-readable description",
"code": "MACHINE_READABLE_CODE"
}
Switch on code for programmatic handling. Surface message to your
end users when appropriate — it's safe to display.
Status codes
| Status | When | code |
|---|---|---|
400 |
Body or query validation failed (e.g. subject missing or too long). |
VALIDATION_ERROR |
401 |
Missing Authorization header, malformed Bearer prefix, or unknown key. |
INVALID_API_KEY |
403 |
API key valid, but the account isn't on a paid plan. | PAID_PLAN_REQUIRED |
403 |
Out of monthly course credits on the current plan. | USAGE_LIMIT_EXCEEDED_PLAN |
404 |
Job or course not found, or belongs to a different key. | NOT_FOUND |
429 |
Rate limit exceeded for this endpoint. | RATE_LIMIT_EXCEEDED |
500 |
Internal error. Report to support if it persists. | INTERNAL_ERROR |
Machine-readable codes
PAID_PLAN_REQUIRED
Returned with 403 when the requesting account isn't on a paid plan.
{ "code": "PAID_PLAN_REQUIRED", "message": "API access requires a paid plan" }
The plan check runs on every request — a key stops working the moment the account is downgraded, and starts working again the moment it's upgraded.
To resolve: upgrade your plan. The existing key becomes valid on the next request after the plan change is processed.
USAGE_LIMIT_EXCEEDED_PLAN
Returned with 403 on POST /api/v1/courses
when the plan's monthly course-credit allowance is exhausted.
{
"code": "USAGE_LIMIT_EXCEEDED_PLAN",
"message": "Usage limit exceeded for your plan (Pro)"
}
The check runs before the job is queued, so an over-quota request
returns 403 immediately without consuming credits or producing a job.
The message includes the plan name to help surface the right upgrade
prompt to your end users.
To resolve: wait for the next billing-period reset, upgrade to a higher tier, or contact support.
Rate limits
Limits are per-key, sliding-window over 60 seconds.
| Endpoint | Limit |
|---|---|
POST /api/v1/courses |
30 / min |
POST /api/v1/courses/:id/export |
30 / min |
GET /api/v1/courses/jobs/:jobId |
120 / min |
When you exceed a limit, the request returns 429 with body:
{ "message": "Too many requests", "code": "RATE_LIMIT_EXCEEDED" }
Standard rate-limit headers are included on every response:
| Header | Meaning |
|---|---|
RateLimit-Limit |
Total requests allowed in the current window. |
RateLimit-Remaining |
Requests left in the current window. |
RateLimit-Reset |
Seconds until the window resets. |
Network errors
Errors not produced by the API itself — TLS failures, connection resets, DNS issues — surface in your HTTP client as transport errors, not as JSON bodies. Treat them as transient.
When retrying POST /api/v1/courses after a
transport error, the original request may have succeeded — you'll see
a course.created webhook for the duplicate as well. Deduplicate on
your side using jobId.