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.
{
"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_request400unauthorized401not_found404rate_limited429insufficient_scope403plan_required403 or 402conflict409internal_error500Rate 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
POST /v1/generate and the generate_course MCP tool.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.
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/completions, /issued-certificates and /identities — the three lists that grow without bound. Default 50, maximum 10,000: ask for all four thousand of your learners in one call if that is what you want. A larger request is served at the maximum rather than refused, and pagination.limit says what you actually got with pagination.hasMore beside it.promptscreensTarget403 plan_required. Generation checks before the model runs, so a refusal never costs an allowance.403 plan_required; what is already stored stays.403 plan_required; a course already over the line, after a downgrade, stays editable so you can edit your way back under it.stackCards413./identities/bulkPOST and DELETE. That is a batch size, not a quota — nothing limits how many calls follow.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.