Guide

Errors

Every refusal uses one envelope and one stable machine code. Branch on the code, never on the message or the wording.

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.

Success
{
  "data": { "id": 14, "name": "Springfield Academy" },
  "meta": { "request_id": "req_01HZY8N2Q0K3M4P5R6S7T8U9V0", "cost": 0.5 }
}
Refusal
{
  "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

StatusCodeWhat happenedWhat to do
401api_key_not_acceptedApp keys are accepted only on /v1.App keys work on /v1 only. Call the documented /v1 path, not a dashboard path.
401api_key_requiredAn app key is required.Send an app key as `Authorization: Bearer ...` on every /v1 call.
401invalid_api_keyNo 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.
401jwt_not_acceptedA 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.
403app_disabledThe app this key belongs to is disabled.Ask a school administrator to re-enable the app in the Developer section.
403app_not_liveA 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.
403kyc_requiredA 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.
403school_deactivatedThe 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.
501test_mode_unavailableA 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

StatusCodeWhat happenedWhat to do
403app_route_not_exposedThis 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.
403app_route_refusedApps cannot use sign-in, session or credential endpoints.Apps cannot call sign-in or credential endpoints. Use the end-user authentication flow instead.
403insufficient_scopeThe 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.
403ip_not_allowedThe 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.
404endpoint_not_exposedThis 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

StatusCodeWhat happenedWhat to do
400invalid_idempotency_keyThe 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.
409idempotency_in_progressThe 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.
422idempotency_key_reusedThis 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

StatusCodeWhat happenedWhat to do
402insufficient_balanceThe 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.
429rate_limitedThe 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

StatusCodeWhat happenedWhat to do
403origin_not_allowedA 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.
403publishable_key_not_allowedA 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

StatusCodeWhat happenedWhat to do
401invalid_credentialsThe 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.
401invalid_end_user_tokenThe 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.
401invalid_otpThe one-time code is wrong or expired.Ask the person for the latest code, or request a new one.
401invalid_refresh_tokenThe refresh token is not valid. Sign the person in again.Sign the person in again. Refresh tokens last 30 days and are used once.
401invalid_two_factor_codeThe two-factor code is wrong or expired.Ask the person for a fresh code and try again.
401refresh_token_reusedA 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.
401two_factor_requiredThe 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.
403account_lockedToo many failed attempts. The account is locked for a while.Ask the person to wait and try again later, or to reset their password.
403account_pending_approvalThe 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.
403end_user_not_eligibleThis 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.
403end_user_token_not_allowedThis 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.
404oauth_request_not_foundThe 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

StatusCodeWhat happenedWhat to do
400school_header_requiredAn 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.
403branch_not_grantedThe 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

StatusCodeWhat happenedWhat to do
400money_out_idempotency_key_requiredEvery money-out write needs an Idempotency-Key header.Send an Idempotency-Key on every money-out write.
403money_out_end_user_token_not_allowedMoney-out is app-only: an end-user token can never move money.Call money-out from your server with the app secret key.
403money_out_grant_requiredThe 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.
403money_out_ip_not_allowlistedThe 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.
403money_out_live_key_requiredMoney-out endpoints refuse test keys. There is no test mode for moving money.Use a live secret key. Money-out has no test mode.
403money_out_org_app_not_allowedOrganization 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

StatusCodeWhat happenedWhat to do
422ai_generation_failedThe generation ended in an error. Nothing was saved; retry later.Nothing was saved. Retry later.
422ai_output_emptyThe model answered but nothing usable came back. Nothing was saved.Nothing was saved. Retry, or make the brief more specific.
429ai_budget_exhaustedThe 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.
429ai_generation_in_progressThis 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

StatusCodeWhat happenedWhat to do
429renderer_busyToo many documents are being rendered right now. Retry after Retry-After.Retry after the Retry-After header. Renders usually finish within seconds.

Messaging

StatusCodeWhat happenedWhat to do
422channel_unavailableThe 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

StatusCodeWhat happenedWhat to do
400invalid_cursorThe 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.
400invalid_parameterA 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.
500internal_errorSomething 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.
503platform_unavailableThe 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

StatusCodeWhat happenedWhat to do
429edge_rate_limitedA 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.