Skip to content

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.

Code example
{
  "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:

Code example
{
  "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

HTTPcodeMeaning
401api_key_missingNo credential presented
401invalid_api_keyUnknown or malformed key
401api_key_revokedThe key was revoked
401api_key_expiredThe key passed its expiry
402subscription_requiredThe account has no active plan, or its trial ended (trial_ended_at)
403origin_not_allowedBrowser-pinned key called from another origin
403insufficient_scopeYour key does not carry the scope this route requires
403lottery_not_entitledThe game exists, but is not in your selection
403plan_upgrade_requiredThe plan does not include this endpoint; required_plan names the cheapest one that does
403history_not_includedThe 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
403export_not_permittedThe key an export was taken with was revoked, expired or lost exports:read, so its link no longer works
404not_foundUnknown URL or unknown record — deliberately identical
410export_expiredThe export's file is gone; ask for a new export
422invalid_filterWith the allowed filters in the message
422invalid_sortWith the sortable fields in the message
422invalid_includeWith the includable relations in the message
422invalid_queryThe query string could not be read
422ticket_check_unsupportedTickets of this game cannot be checked yet (ticket_check.kind is null on the game)
422distributions_unsupportedThe 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)
422generator_unsupportedTickets cannot be generated for this game: bingo-style games, games drawn from several drums, and races (tools.generate is false on the game)
422generator_impossibleNo ticket can meet these filters together; the message says which one cannot be met
422generator_space_too_smallThe filters allow fewer different tickets than count asks for; the message says how many
422generator_filters_too_narrowNo generated ticket passed the filters (or every one that did was already drawn): loosen them
422wheel_unsupportedReduced 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)
422wheel_too_largeThe reduced system asked for is too large to work out: choose fewer numbers or another guarantee
422not_enough_drawsThe filters read from past draws need at least 100 draws in window
422ticket_check_too_largeToo many draws for this many tickets at once: check fewer tickets or a shorter window
422webhook_limit_reachedThe key already holds as many webhook subscriptions as it may
422delivery_still_retryingThe delivery is still being retried and cannot be replayed yet
429rate_limit_exceededPer-minute burst. retry_after is in seconds
429quota_exceededDaily quota spent. Retry after the UTC reset, not in a second
429stream_limit_reachedThis account already holds as many live streams as it may
429stream_capacity_reachedThe stream is at capacity across all clients
429export_limit_reachedThe account has taken every export its plan includes this month; resets_at says when the allowance renews
429export_in_progressThe 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:

Code example
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.