On this page
Every failure from /api/v1 is an RFC 9457 problem document served as application/problem+json. The code field is the stable identity of the failure and is what your client should branch on. detail is prose written for a human reading a log; it gets reworded, so treating it as an interface will break you.
The type URL on every problem points at an anchor on this page, so pasting it into a browser lands on the explanation.
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 30
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
{
"type": "https://crossword.texs.org/developers/errors#RATE_LIMITED",
"title": "Rate limited",
"status": 429,
"code": "RATE_LIMITED",
"detail": "60 requests per minute exceeded",
"limit": 60,
"remaining": 0
}Treat a code you do not recognise as a generic failure of its status class — new codes get added, and a client that throws on an unknown one is a client that breaks on a Tuesday.
UNAUTHORIZED
No key was presented, or the key presented is unknown, revoked, or malformed. The API does not distinguish between those cases in the response — a revoked key and a typo look identical from outside.
Check the header reads Authorization: Bearer cw_live_… (or X-Api-Key: cw_live_…) with no stray quotes or whitespace. Revoking is permanent; a revoked key never starts working again, so mint a fresh one from your dashboard.
FORBIDDEN_SCOPE
The key is real and active but does not carry the scope this operation needs. requiredScope names the missing one.
New keys carry all three scopes, so a key that lacks one was narrowed deliberately. Either call an endpoint inside the key’s scopes or mint a second key for the job.
Carries: requiredScope
GRID_SIZE_LOCKED
The grid is larger than the account’s ceiling — 13×13 on the free tier, 23×23 for members. maxGridSize carries the ceiling that applied. This is the same gate the web constructor enforces, deliberately: the API is not a way around it.
Send a grid at or under maxGridSize, or upgrade the account the key belongs to — tier names the plan it is on and upgradeUrl is where to go; show both to the person. Check the size before you spend a solve — the grid is an array of rows, so its length is the size.
Carries: maxGridSize, tier, upgradeUrl
MEMBERS_ONLY
The feature needs a paid membership. Today that is only POST /clues/generate: the free tier’s AI clue allowance is zero rather than small, because each call costs real model spend.
Use GET /clues/{word} and POST /clues/bulk instead — the corpus is open to every key and already has clues for most common answers — or show the person upgradeUrl: any membership unlocks it.
Carries: tier, upgradeUrl
AI_QUOTA_REACHED
The account’s monthly AI-clue allowance is spent. It is the same pool the web app draws from, so clues generated in the constructor count against the API and vice versa.
Watch X-Ai-Clues-Remaining on every successful generate call (-1 means unlimited) rather than waiting for the 429. The pool refills on the first of the calendar month. detail lists the plans above tier and what each allows; show the person upgradeUrl, or contact when they are already on the top plan.
Carries: limit, remaining, tier, upgradeUrl, contact
FILL_QUOTA_REACHED
The account’s monthly fill allowance is spent. A clean-up (POST /fill/improve) counts as a fill: it runs the same solver for the same wall-clock budget.
Watch X-Fill-Quota-Remaining on each solve. Pattern generation and .puz export cost nothing, so a client that generates patterns locally and fills only the ones worth filling goes a long way on a small allowance. When the wall is real, detail lists the plans above tier and what each allows; show the person upgradeUrl, or contact when they are already on the top plan.
Carries: limit, remaining, tier, upgradeUrl, contact
PUZZLE_QUOTA_REACHED
The account has created its month’s allowance of new puzzles through POST /puzzles. The numbers match the fill allowance — a puzzle is what a fill is for — and exist to bound a runaway integration (a hook that fires on every edit), not to sell puzzles.
First check you are not creating twins: send an Idempotency-Key per thing you create and a repeat costs nothing. Editing (PATCH) and publishing existing puzzles are never metered. If the volume is real, detail lists the plans above tier; show the person upgradeUrl, or contact on the top plan.
Carries: limit, remaining, tier, upgradeUrl, contact
SHOWCASE_QUEUE_FULL
submitToShowcase: true while queued of the account’s puzzles are already waiting for showcase review (limit is three). A person reads every API submission — nothing auto-approves — so the cap is what keeps a bulk submission from outrunning the reader. Nothing about the request was applied.
Publish the puzzle unlisted now (omit submitToShowcase) — it gets its page and embed frame either way — and call /publish again with submitToShowcase: true once one of the queued puzzles is reviewed. Submit the puzzles you would put your name to; the queue is not a channel for everything you generate.
Carries: limit, queued
RATE_LIMITED
Too many requests in the current minute for this key’s tier. This is a short window, not a quota — nothing has been consumed.
Sleep for Retry-After seconds and retry. If you are looking up clues for a whole grid, one POST /clues/bulk call replaces seventy-odd GET /clues/{word} calls and will not trip this at all.
Carries: limit, remaining
SOLVER_BUSY
The solver is already running as many jobs as it will run at once — globally, across all API callers, or two for your account. Fills hold a CPU for up to half a minute, so the cap is what keeps one caller from starving everyone else.
Retry after a few seconds with some jitter. Nothing was charged against your fill quota, and a queued retry almost always gets in. Do not run more than two concurrent fills per account — the third is refused, not queued. Extra API keys do not buy extra concurrency; every limit is keyed to the account that owns them.
LANGUAGE_UNAVAILABLE
400 — the language code is not one the service knows. 503 — the code is real but its word index could not be loaded. The indexes are large (Turkish alone is over a gigabyte) and share a memory budget, so they are evicted and reloaded on demand.
For a 400, check the code against GET /languages. For a 503, retry once after a few seconds — the first request for a cold language pays a multi-second load, and GET /status shows which indexes are resident right now.
VALIDATION_ERROR
The request body or query string did not validate. field names the offending field and detail says what is wrong with it, down to the row and column for a grid.
The two that catch people out: a grid whose rows are not all exactly as long as the grid is tall, and a letter that is not in the puzzle language’s alphabet (use . for an empty cell and # for a black square). Retrying unchanged will fail identically.
Carries: field
NOT_FOUND
No such resource — or a draft belonging to somebody else. That case is a 404 rather than a 403 on purpose: the API does not confirm that an id it will not show you exists.
Check the id. Published puzzles are readable by any key; drafts are readable only by the account that owns them.
UPSTREAM_UNAVAILABLE
The solver box or the puzzle store could not be reached. Nothing was charged against a quota, and nothing was written.
Retry with exponential backoff. If it persists, GET /status is unauthenticated and will tell you whether the solver is answering at all.
Back to the reference
The developer documentation has the quickstart, the grid encoding, the rate-limit table, and the full endpoint reference.