The envelope
A successful call returns a data payload and a meta object. A refusal returns an error object and the same meta. The request id is also sent in the X-Request-ID response header, on both.
{
"data": { "id": 14, "name": "Springfield Academy" },
"meta": { "request_id": "req_01HZY8N2Q0K3M4P5R6S7T8U9V0", "cost": 0.5 }
}{
"error": {
"code": "insufficient_scope",
"message": "The app does not hold the scope this endpoint needs.",
"field": null
},
"meta": { "request_id": "req_01HZY8N2Q0K3M4P5R6S7T8U9V0" }
}The optional field names the request field at fault when there is one, so a validation refusal can be shown next to the input that caused it.
Every code
The tables below are generated from the API itself, so they list every code a /v1 call can answer with, grouped by what caused it. Only a 429, a 500 or a 503 is worth retrying as it is, with the same Idempotency-Key. Every other code needs something to change first.
Authentication and keys
| Status | Code | What happened | What to do |
|---|---|---|---|
| 401 | api_key_not_accepted | App keys are accepted only on /v1. | App keys work on /v1 only. Call the documented /v1 path, not a dashboard path. |
| 401 | api_key_required | An app key is required. | Send an app key as `Authorization: Bearer ...` on every /v1 call. |
| 401 | invalid_api_key | No key was presented, or the key is not valid (unknown, revoked or expired). | Check the Authorization header is `Bearer ks_live_sk_...`. If the key was rotated or revoked, issue a new one in the Developer section. |
| 401 | jwt_not_accepted | A dashboard session token was sent to the API. Use an app key. | A dashboard sign-in token cannot call the API. Use an app key, or an end-user token issued to your app. |
| 403 | app_disabled | The app this key belongs to is disabled. | Ask a school administrator to re-enable the app in the Developer section. |
| 403 | app_not_live | A live key works only after a school administrator has taken the app live. | Keep building on the test key, and ask a school administrator to take the app live. |
| 403 | kyc_required | A live key needs the school's own verification (KYC) to be approved. | The school must finish its verification in the dashboard before any live key works. Test keys keep working meanwhile. |
| 403 | school_deactivated | The key's school is deactivated, so none of its apps can call the API. | The school is not active on Klasstack. Contact the school or support. |
| 501 | test_mode_unavailable | A test key called an endpoint that has no test response, such as MCP. Use a live key. | This endpoint (such as MCP) has no test response. Call it with a live key once the app is live. |
Scopes and access
| Status | Code | What happened | What to do |
|---|---|---|---|
| 403 | app_route_not_exposed | This endpoint is not available to this kind of caller (key type, end-user token or role). | This endpoint does not accept this kind of credential. Check the reference for which key or token it needs. |
| 403 | app_route_refused | Apps cannot use sign-in, session or credential endpoints. | Apps cannot call sign-in or credential endpoints. Use the end-user authentication flow instead. |
| 403 | insufficient_scope | The app (or, with an end-user token, the person) does not hold the scope this endpoint needs. | Tick the service and the read or write access the endpoint needs on the app, then retry. Write, billable and money-out services need an admin to approve. |
| 403 | ip_not_allowed | The app has an IP allowlist and the caller's address is not on it. | Call from an address on the app's IP allowlist, or ask an administrator to add yours in the Developer section. |
| 404 | endpoint_not_exposed | This endpoint is not part of the API, or is switched off. | Check the method and the path against the reference. An endpoint the platform has switched off answers the same way, on purpose. |
Idempotency
| Status | Code | What happened | What to do |
|---|---|---|---|
| 400 | invalid_idempotency_key | The Idempotency-Key is empty or longer than 200 characters. | Send a fresh key of at most 200 characters, such as a UUID v4 generated before the first attempt. |
| 409 | idempotency_in_progress | The first request with this key has not finished yet. Retry shortly. | Do not retry the same key in a loop. The write may already have happened, so read the resource back to see whether it landed, and only then decide to retry with a new key. |
| 422 | idempotency_key_reused | This key was already used for a request with a different method, path, query or body. | Use a fresh key for a different operation, and keep one key bound to one request body. |
Rate limits and balance
| Status | Code | What happened | What to do |
|---|---|---|---|
| 402 | insufficient_balance | The school's service wallet cannot cover this call. | Top up the service wallet, or wait for next month to refresh the plan credit. Test calls are free, so test mode keeps working. |
| 429 | rate_limited | The rate limit was reached. Retry after the Retry-After header says. | Back off and retry after the number of seconds in the Retry-After header. X-RateLimit-Remaining tells you how much of the window is left. |
Publishable keys and CORS
| Status | Code | What happened | What to do |
|---|---|---|---|
| 403 | origin_not_allowed | A publishable key was used from an origin or bundle id its app does not list. | Add the web origin or mobile bundle id to the app in the Developer section. A mobile app sends its bundle id in X-Klasstack-Bundle-Id. |
| 403 | publishable_key_not_allowed | A publishable key can reach only the public endpoints. Use a secret key on your server. | Publishable keys reach the public endpoints only. Make this call from your server with a secret key. |
End-user sign-in and tokens
| Status | Code | What happened | What to do |
|---|---|---|---|
| 401 | invalid_credentials | The identifier and password (or OTP) do not match a person in this school. | Show a generic sign-in failure to the person. Do not say which half was wrong. |
| 401 | invalid_end_user_token | The end-user token is malformed, expired, for another app, or its session was ended. | Refresh the access token with the refresh token. If that fails too, sign the person in again. |
| 401 | invalid_otp | The one-time code is wrong or expired. | Ask the person for the latest code, or request a new one. |
| 401 | invalid_refresh_token | The refresh token is not valid. Sign the person in again. | Sign the person in again. Refresh tokens last 30 days and are used once. |
| 401 | invalid_two_factor_code | The two-factor code is wrong or expired. | Ask the person for a fresh code and try again. |
| 401 | refresh_token_reused | A refresh token was used twice. The whole session has been ended; sign the person in again. | Every token in that session is now ended. Sign the person in again, and make sure only one place in your app refreshes. |
| 401 | two_factor_required | The person has two-factor sign-in on. Send their current code as well. | Ask the person for their current two-factor code and send it as twoFactorCode. |
| 403 | account_locked | Too many failed attempts. The account is locked for a while. | Ask the person to wait and try again later, or to reset their password. |
| 403 | account_pending_approval | The account is still waiting for the school to approve it. | The school has not approved this account yet. Ask the person to contact the school. |
| 403 | end_user_not_eligible | This account cannot sign in to apps (for example an applicant, or a suspended account). | Only parents, students and staff can sign in to apps. Applicants and suspended accounts cannot. |
| 403 | end_user_token_not_allowed | This endpoint (such as the login API) needs the app's secret key, not an end-user token. | Call this endpoint from your server with the app secret key, not with the person token. |
| 404 | oauth_request_not_found | The hosted sign-in request has expired or was already used. Start again. | Sign-in requests last ten minutes and work once. Start the authorize redirect again. |
Organization apps and branches
| Status | Code | What happened | What to do |
|---|---|---|---|
| 400 | school_header_required | An organization app must name its branch in the X-Klasstack-School header. | Send X-Klasstack-School with the id of a branch granted to the app. |
| 403 | branch_not_granted | The branch named in X-Klasstack-School is not granted to this organization app. | Ask the organization administrator to grant the app this branch, or name a branch it already holds. |
Money-out
| Status | Code | What happened | What to do |
|---|---|---|---|
| 400 | money_out_idempotency_key_required | Every money-out write needs an Idempotency-Key header. | Send an Idempotency-Key on every money-out write. |
| 403 | money_out_end_user_token_not_allowed | Money-out is app-only: an end-user token can never move money. | Call money-out from your server with the app secret key. |
| 403 | money_out_grant_required | The moneyOut scope must be granted by a school administrator who confirms with a step-up check. | A school administrator must grant the moneyOut scope and confirm with a step-up check. |
| 403 | money_out_ip_not_allowlisted | The app has no IP allowlist, or the caller's address is not on it. | Ask an administrator to add your server addresses to the app IP allowlist. Money-out refuses every call while the list is empty. |
| 403 | money_out_live_key_required | Money-out endpoints refuse test keys. There is no test mode for moving money. | Use a live secret key. Money-out has no test mode. |
| 403 | money_out_org_app_not_allowed | Organization apps cannot move money. Use an app registered by the school itself. | Register an app under the school itself for money-out. Organization apps can read but not move money. |
AI
| Status | Code | What happened | What to do |
|---|---|---|---|
| 422 | ai_generation_failed | The generation ended in an error. Nothing was saved; retry later. | Nothing was saved. Retry later. |
| 422 | ai_output_empty | The model answered but nothing usable came back. Nothing was saved. | Nothing was saved. Retry, or make the brief more specific. |
| 429 | ai_budget_exhausted | The school's shared daily AI allowance is used up. Retry-After is the seconds until it resets. | Wait for the school's daily allowance to reset. Retry-After says how many seconds that is. |
| 429 | ai_generation_in_progress | This app already has the maximum number of AI generations running. | Wait for a running generation to finish, then retry after Retry-After. |
Documents and the renderer
| Status | Code | What happened | What to do |
|---|---|---|---|
| 429 | renderer_busy | Too many documents are being rendered right now. Retry after Retry-After. | Retry after the Retry-After header. Renders usually finish within seconds. |
Messaging
| Status | Code | What happened | What to do |
|---|---|---|---|
| 422 | channel_unavailable | The school cannot send on this channel right now. Nothing was sent to anyone. | Check the school has this channel set up and funded, or send on another channel. |
Generic
| Status | Code | What happened | What to do |
|---|---|---|---|
| 400 | invalid_cursor | The cursor is not one this API issued. Send next_cursor back unchanged. | Send next_cursor back exactly as it was given. Never build or edit a cursor. |
| 400 | invalid_parameter | A request parameter is invalid; `field` names it. | Fix the parameter named in `field`. A write without an Idempotency-Key is refused this way with field set to Idempotency-Key. |
| 500 | internal_error | Something failed on our side. Retry later and quote the request id if it persists. | Retry with backoff and the same Idempotency-Key. If it keeps happening, send support the request id. |
| 503 | platform_unavailable | The API is not available for this school right now. | The platform release flag or the school status is off. Retry later, and contact support if it persists. |
Before the API
| Status | Code | What happened | What to do |
|---|---|---|---|
| 429 | edge_rate_limited | A coarse flood guard in front of the API refused the call before it was authenticated. | This one arrives as a plain JSON body rather than the error envelope, because it never reaches the API itself. It only triggers on a flood from a single IP address. Slow down, then retry. |
Request ids
Every response carries X-Request-ID, and the same value appears as meta.request_id. Log it. It is the id the call is recorded under in the Developer section request log, and quoting it is the fastest way to get an answer from support. Call metadata is kept for 90 days. Request and response bodies are never stored.