Skip to content
Menu
Guide

Errors

One body shape for every failure, a stable code that does not change meaning, and the correct HTTP status. Rate limiting is a 429, not a 400 dressed up as one.

The error body

The OpenAPI document declares a single Error schema, and every failure uses it: an error object with a required code and message, and an optional details. The code is a string a client can branch on; the message is for a human and may grow more specific without the code changing.

{
  "error": {
    "code": "quota_exceeded",
    "message": "Daily quota exceeded for the \"free\" plan on the \"words\" endpoint: 1001 of 1000 requests used today. Quota resets at 00:00 UTC. Upgrade your plan at /developers for a higher limit.",
    "details": { "plan": "free", "endpoint": "words", "used": 1001, "limit": 1000 }
  }
}

The quota example carries details because that path sets it. Most failures do not — missing_api_key, for one, is only code and message:

{
  "error": {
    "code": "missing_api_key",
    "message": "No API key supplied. Send your key in the X-API-Key header. Get a free key at /developers."
  }
}

Internal failures are deliberately vague. A handler that throws returns internal_error with the message “Something went wrong handling that request.” — never a stack trace and never a database error.

Every code, with its status

The codes and the statuses are read from the shared table in @ozituma/core, which is what lib/api.ts and every route consult, so this table cannot go out of step with the responses.

One row per code in the API’s error table.
StatusCodeWhen
401missing_api_keyNo key was sent. readApiKey looks for X-API-Key first, then Authorization: Bearer <key>, and rejects the request when neither carries a value.
401invalid_api_keyThe key is not known, has been revoked, has expired, or its developer is suspended. authenticateApiKey returns null for all four, so revocation is reported through this code.
401revoked_api_keyDefined in the shared code table and mapped to 401. The current authenticateApiKey path collapses revoked, expired and unknown keys into invalid_api_key, so this code is not emitted today.
429quota_exceededThe key has used its whole daily allowance for the metered endpoint. The response carries Retry-After in seconds until 00:00 UTC and a details object with plan, endpoint, used and limit.
429rate_limitedPOST /api/v1/developers only: more than 5 key requests from one IP address in a 15-minute window. The response carries Retry-After: 900.
404not_foundNo entry matches that id or slug, or no word of the day exists for the requested language.
400invalid_parameterA query or body parameter is missing, malformed, or outside its range. The message names the parameter.
400unsupported_languageThe language value is not an active language code or URL slug. The message lists the codes that are available.
500internal_errorAn unexpected server error. The message is generic on purpose; a stack trace or a SQL error is never returned to a caller.

The two 429s are different

Both are HTTP 429 and both mean “slow down”, but they are not the same limit and a client should treat them differently.

  • quota_exceeded — a metered request has used the key’s whole daily allowance for that endpoint. Retry-After is the number of seconds until 00:00 UTC. Waiting is the only fix.
  • rate_limited — POST /api/v1/developers only. More than five key requests have come from one IP address in a fifteen-minute window. Retry-After is 900, the whole window.

The limits themselves →

Headers come with failures too

A failure from a metered route still carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Used once the key has been resolved — a quota rejection, a 404, a bad parameter — so a client that handles the error has the numbers it needs without a second call. The two 401s are the exception: a request with no key, or an unusable one, is refused before any quota is computed, so it carries no rate-limit headers.