API error codes

What each code means, and whether retrying is worth your time.
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.

A rate-limit response in full
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

401Unauthorized·Retrying will not help

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

403Forbidden·Retrying will not help

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

403Grid size locked·Retrying will not help

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

403Members only·Retrying will not help

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

429AI clue quota reached·Retry next calendar month

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

429Fill quota reached·Retry next calendar month

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

429Puzzle quota reached·Retry next calendar month

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

409Showcase queue full·Retry once a queued puzzle is reviewed

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

429Rate limited·Retry after a short wait

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

429Solver busy·Retry after a short wait

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 / 503Language unavailable·Retry after a short wait

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

400Validation error·Retrying will not help

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

404Not found·Retrying will not help

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

502 / 503Upstream unavailable·Retry after a short wait

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.

Terms·Privacy·texs.org