API essentials
Handle the unexpected.
Stable codes to branch on, and when to retry.
Every failure carries a translated message, and every failure but a plain validation error
also carries a stable code. Branch on the code — the message is translated server-side
and will change.
{
"message": "This API key is not entitled to this lottery.",
"code": "lottery_not_entitled"
}
A validation failure — a parameter the API could not accept — has no code. It is a 422
carrying Laravel's errors map instead, keyed by the parameter:
{
"message": "This is not a date the API can read.",
"errors": { "filter.from": ["This is not a date the API can read."] }
}
An undeclared filter, sort or include carries both: one of the invalid_* codes below, and an
errors entry under filter, sort or include.
The codes
| HTTP | code | Meaning |
|---|---|---|
| 401 | api_key_missing | No credential presented |
| 401 | invalid_api_key | Unknown or malformed key |
| 401 | api_key_revoked | The key was revoked |
| 401 | api_key_expired | The key passed its expiry |
| 402 | subscription_required | The account has no active plan, or its trial ended (trial_ended_at) |
| 403 | origin_not_allowed | Browser-pinned key called from another origin |
| 403 | insufficient_scope | Your key does not carry the scope this route requires |
| 403 | lottery_not_entitled | The game exists, but is not in your selection |
| 403 | plan_upgrade_required | The plan does not include this endpoint; required_plan names the cheapest one that does |
| 403 | history_not_included | The result is older than the history your plan includes — or, on a sandbox key, newer than its seven-day delay or older than the latest 50 results |
| 403 | export_not_permitted | The key an export was taken with was revoked, expired or lost exports:read, so its link no longer works |
| 404 | not_found | Unknown URL or unknown record — deliberately identical |
| 410 | export_expired | The export's file is gone; ask for a new export |
| 422 | invalid_filter | With the allowed filters in the message |
| 422 | invalid_sort | With the sortable fields in the message |
| 422 | invalid_include | With the includable relations in the message |
| 422 | invalid_query | The query string could not be read |
| 422 | ticket_check_unsupported | Tickets of this game cannot be checked yet (ticket_check.kind is null on the game) |
| 422 | distributions_unsupported | The draw distributions do not fit this game: bingo-style games, whose count of numbers is the result, and games drawn from several drums (statistics.distributions is false on the game) |
| 422 | generator_unsupported | Tickets cannot be generated for this game: bingo-style games, games drawn from several drums, and races (tools.generate is false on the game) |
| 422 | generator_impossible | No ticket can meet these filters together; the message says which one cannot be met |
| 422 | generator_space_too_small | The filters allow fewer different tickets than count asks for; the message says how many |
| 422 | generator_filters_too_narrow | No generated ticket passed the filters (or every one that did was already drawn): loosen them |
| 422 | wheel_unsupported | Reduced systems are built only for games whose ticket is a set of different numbers: not digit games, games drawn from several drums or races (tools.wheel is false on the game) |
| 422 | wheel_too_large | The reduced system asked for is too large to work out: choose fewer numbers or another guarantee |
| 422 | not_enough_draws | The filters read from past draws need at least 100 draws in window |
| 422 | ticket_check_too_large | Too many draws for this many tickets at once: check fewer tickets or a shorter window |
| 422 | webhook_limit_reached | The key already holds as many webhook subscriptions as it may |
| 422 | delivery_still_retrying | The delivery is still being retried and cannot be replayed yet |
| 429 | rate_limit_exceeded | Per-minute burst. retry_after is in seconds |
| 429 | quota_exceeded | Daily quota spent. Retry after the UTC reset, not in a second |
| 429 | stream_limit_reached | This account already holds as many live streams as it may |
| 429 | stream_capacity_reached | The stream is at capacity across all clients |
| 429 | export_limit_reached | The account has taken every export its plan includes this month; resets_at says when the allowance renews |
| 429 | export_in_progress | The account is already building as many exports as it may at once; retry when one is ready |
A widget answers with a notice inside its frame rather than JSON; its codes (widget_*) are listed
on Widgets.
Retrying
A 429 carries retry_after in seconds and a Retry-After header. The two request-limit codes
mean different things and deserve different handling: rate_limit_exceeded clears within the
minute, while quota_exceeded does not clear until UTC midnight — retrying in a second
wastes the request and is counted anyway.
Unknown records and unknown URLs answer identically on purpose. Telling them apart would let a caller enumerate what exists.
Request IDs
Every answer past authentication carries an X-Request-Id header, the successful ones
included:
X-Request-Id: 01a115a8-4360-7128-8584-0bce749aabfa
Log it next to your own error. When you write to support about a failed
call, the request ID is what lets us find that exact call — the request log's Ask support
about this call fills it in for you. It is also how you find the call yourself: the
Request log in your workspace keeps every request for 30 days,
with its status, response time and the code and message it returned, and you can search
it by request ID.
A 401, or a 403 origin_not_allowed, answers before the key is accepted, so it carries no
request ID and is not in the log.
Is it us?
The status page shows, checked every minute, whether the API is answering, whether new results are arriving on time and whether webhooks are leaving the queue. When it reports a disruption, there is nothing to fix on your side.