APIHorseRacing

Documentation / Guides

Errors

Every code, what causes it, and what to do about it.

Every failure returns JSON with an error object and no data. Branch on error.code, never on the message: codes are part of the contract, messages are wording and may be improved.

{
  "error": {
    "code": "outside_window",
    "message": "That date is outside the 24h – 7d your plan reads.",
    "doc_url": "https://apihorseracing.com/documentation/errors#outside_window",
    "request_id": "17d0af45d676676d",
    "your_plan": "free",
    "window": "24h – 7d",
    "requested": "2021-02-28"
  }
}

The one distinction that matters

Before the table: most of these are not transient. A client that retries everything on a backoff will burn its own quota against a wall. Only 429 and 500 are worth retrying at all.

Every code

CodeHTTPCauseDo
unauthorized401 No key, or one we do not recognise. Check the header name is X-API-Key and that the key has not been rotated from your account page. Never retry.
upgrade_required403 Valid key, but the plan does not reach this endpoint. Always a statistics endpoint. The body carries your_plan and reaches_on. Show the user which plan would work. Never retry.
outside_window403 The date is outside what your plan reads. The body names window and requested. Clamp your own request or upgrade. Never retry.
forbidden403 The account is disabled or the key revoked. Contact support. This is never a rate limit.
not_found404 The identifier is well formed but nothing has it, or a race exists with no result yet. Treat as an answer, not a failure. Never retry.
invalid_id422 The identifier is malformed, or its prefix is wrong for this path. You have probably passed a horse id where a race is wanted. See Identifiers.
invalid_param422 A parameter failed validation. The message names it. A date that is not YYYY-MM-DD, a by= dimension we do not have, or a cursor you constructed.
rate_limited429 Past your per-minute rate. Retry after Retry-After seconds. Slow down rather than parallelising harder.
quota_exceeded429 Past your monthly allowance. Resets on the first. Retrying before then is pointless. Check /v1/account/usage.
not_yet_live503 Registered and documented, but not serving yet. Watch status.
server_error500 Ours, not yours. Retry once with a backoff. If it persists, send us the request_id.

Handling it properly

switch ($err['code'] ?? '') {

    case 'rate_limited':
        sleep((int) ($headers['retry-after'] ?? 5));
        // retry the same request
        break;

    case 'server_error':
        // retry once, then give up and log request_id
        break;

    case 'outside_window':
    case 'upgrade_required':
        // a plan decision, not a failure. Surface it, do not retry.
        break;

    case 'not_found':
        // an answer. Store the absence.
        break;

    default:
        // your bug. Log it with request_id and stop.
}

The three headers worth reading

  • X-RateLimit-Remaining on every successful response. Slow down before you hit zero rather than after.
  • Retry-After on a 429. Respect it rather than backing off blindly.
  • X-Request-Id on everything. Log it. It costs nothing and it is the difference between us finding your problem in a minute and not at all.

What is never an error

An empty array. If we looked and there was nothing, that is a 200 with data: []: no racing that day, no runs in your window, no dividends published. Treating an empty result as a failure and retrying it is the most common mistake we see.