DocsThe APIErrors & limits

Errors & limits

What went wrong, what it means, and what to do — plus every number the API will enforce on you.

Every error is JSON with the same two keys. error is a stable machine-readable code — safe to branch on, it will not change wording. message is for a human reading a log.

400json
{
  "error": "invalid_request",
  "message": "Invalid option: expected one of \"deferred\"|\"immediate\"",
  "issues": [
    { "code": "invalid_value", "path": ["quizFeedback"], "message": "" }
  ]
}

issues appears on validation failures only, and comes straight from the schema — path is the field that was wrong. It is the fastest way to fix a 400 and most people never look at it.

Every error code

invalid_request400
The body didn't validate. Read the `issues` array — it names the exact field and what was wrong with it. A reference *inside* a body that points at another workspace — a `themeId`, a `certificateId` — lands here too, since the field is what is wrong. Fetching another workspace's object by id is a 404 instead; see below.
unauthorized401
No key, a wrong key, or a revoked one. The header must read `Authorization: Bearer sk_live_…`. Check for a missing `Bearer`, a stray newline from a copy-paste, or a key you revoked.
not_found404
Nothing with that id in your workspace. Note the second half: another workspace's course is a 404 here, not a 403. We don't confirm that someone else's id exists.
rate_limited429
Too many requests, or over your monthly generation allowance. Wait the number of seconds in the `Retry-After` header and try again. For generation, the message names the limit and it resets next month.
insufficient_scope403
A read-only key tried to write. The key is valid — it just cannot do this. Read keys are refused anything but GET, HEAD and OPTIONS. Create a key with write access, or use a different one.
plan_required403 or 402
403: this endpoint needs a higher plan. 402: a Sandbox workspace has spent its monthly allowance, or any workspace has an unpaid invoice past its grace period. The message says which. A cap clears at the start of next month or immediately on upgrading; an unpaid invoice clears the moment it is paid.
internal_error500
Our fault. Retry once. If it persists, the request wasn't the problem — get in touch.

Rate limits

120 requests per key per rolling minute. Going over returns 429 with a Retry-After header in seconds — respect it rather than retrying immediately, which just spends the next window too.

The limit is per key, so splitting a bulk job across two keys doubles it. That is not a loophole we mind; it is what separate keys are for.

What each plan unlocks

Every plan
Courses, screens and blocks, identities, collections, completions, issued certificates, webhooks, the embed, and SCORM export from the dashboard.
Build and up
AI generation — POST /v1/generate and the generate_course MCP tool.
Scale and up
Themes, translations, certificate templates, and SCORM in and out over the API.

Anything above your plan returns 403 plan_required naming the plan it needs. Nothing is silently degraded — you never get a partial result because of your tier.

Monthly allowances

Metering is by course view: one learner opening a published course, counted per calendar month in UTC. One learner opening six courses is six views, and opening the same course six times is six views — it is traffic served, not people reached, which is why learners are unlimited on every plan.

Sandbox
500 course views per month. 5 AI generations per month.
Build
25,000 course views per month. 100 AI generations per month.
Scale
150,000 course views per month. 500 AI generations per month.
Partner
1,000,000 course views per month. Unlimited AI generations.

Only Sandbox is hard-capped for usage: past its allowance every endpoint returns 402 until the month turns over or the workspace upgrades. A paid plan is never cut off for going over — extra views are charged at the published rate instead, and we say so before they are.

A paid plan can still be stopped for non-payment. A failed charge starts a grace period counted down on the billing page; if it runs out, published courses stop serving and the API returns 402 until the invoice is settled. Nothing is deleted or downgraded, and paying restores it immediately.

Sizes and counts

limit / offset
On /completions, /issued-certificates and /identities — the three lists that grow without bound. Default 50 and no maximum: ask for all four thousand of your learners in one call if that is what you want. The ceiling is how much data you have, and pagination.hasMore is there if you would rather page.
prompt
Up to 2,000 characters.
screensTarget
Up to 20 screens per generation.
stackCards
Up to 10 cards on a stack block.
SCORM import
Packages up to 100MB. Larger returns 413.
CSV import
A course CSV takes up to 2,000 data rows; the learner importer in the dashboard takes 1,000.
/identities/bulk
Up to 500 learners per call, on both POST and DELETE. That is a batch size, not a quota — nothing limits how many calls follow.
Uploads
Media up to 25MB, fonts up to 5MB. Dashboard only — the API takes URLs, not files.

Things that look like errors and aren't

A draft course’s embed link 404s. That is the same response as a course that never existed, on purpose — an unpublished course shouldn’t be discoverable by trying ids. Publish it.

A certificate request can 403. The learner completed the course but didn’t meet its passingScore. That is the feature working.

A block rendering blank is almost always a misspelled field name. Nothing rejects unknown fields, so "lable" saves happily and shows nothing. Check it against the block reference.

A completion with a null score means the course has no scored blocks. It isn’t zero — it was never a test.