Errors
#Errors
Every response on /api/v1 other than the token endpoint is application/problem+json on failure:
{
"type": "https://sailracing.club/docs/errors#API_TOKEN_INVALID",
"status": 401,
"code": "API_TOKEN_INVALID",
"params": {},
"traceId": "00-4bf9…-01"
}type is this page, anchored at the code (the anchors below match exactly). params is only present when
the code carries one (e.g. API_SCOPE_MISSING's scope). There is no title/detail sentence to parse —
build your own copy from the code, using the table below.
The token endpoint follows RFC 6749 instead (an error field, not type/status) — see
Get access token for its error shape specifically.
#Quick table
| HTTP | Code | Where |
|---|---|---|
| 400 | API_TOKEN_REQUEST_INVALID |
token endpoint |
| 400 | API_GRANT_TYPE_UNSUPPORTED |
token endpoint |
| 401 | API_CLIENT_CREDENTIALS_INVALID |
token endpoint |
| 400 | API_SCOPE_INVALID |
token endpoint |
| 401 | API_TOKEN_MISSING |
data calls |
| 401 | API_TOKEN_INVALID |
data calls |
| 401 | API_CLIENT_DISABLED |
token endpoint + data calls |
| 401 | API_CLIENT_EXPIRED |
token endpoint + data calls |
| 403 | API_IP_NOT_ALLOWED |
token endpoint + data calls |
| 403 | API_SCOPE_MISSING |
data calls |
| 400 | API_PARAM_INVALID |
list paging |
| 404 | EVENT_NOT_FOUND |
event/race lookups |
| 404 | RACE_NOT_FOUND |
race lookups |
| 429 | RATE_LIMIT_EXCEEDED |
any call |
| 503 | API_UNAVAILABLE |
data calls |
| 500 | INTERNAL_ERROR |
any call |
#Token endpoint errors
#API_TOKEN_REQUEST_INVALID
400. The request wasn't a valid client_credentials form post — e.g. credentials sent in both the
Authorization header and the form body, or no Content-Type: application/x-www-form-urlencoded. Fix:
send exactly one of Basic auth or form-body credentials, as application/x-www-form-urlencoded.
#API_GRANT_TYPE_UNSUPPORTED
400. grant_type was missing or wasn't client_credentials (this API doesn't support any other grant).
Fix: always send grant_type=client_credentials.
#API_CLIENT_CREDENTIALS_INVALID
401. The client_id is unknown, or the client_secret is wrong — the same answer either way, so a
brute-force attempt can't tell which half failed. Fix: double-check both values with the club admin; if
correct, the secret may have been rotated — ask for a fresh one.
#API_SCOPE_INVALID
400. The scope= you asked for at the token endpoint includes something the client doesn't hold (or
doesn't exist). params.scope names the offending value. Fix: request a subset of the client's actual
scopes, or omit scope to get all of them. See Scopes.
#Shared (token endpoint + data calls)
#API_CLIENT_DISABLED
401 on data calls, 400 (unauthorized_client) on the token endpoint. The club admin disabled this
client. Fix: ask the club admin to re-enable it (Settings → API access) — nothing you can do from the
integration side.
#API_CLIENT_EXPIRED
401 on data calls, 400 (unauthorized_client) on the token endpoint. The client passed its
expiresAt date. Fix: ask the club admin to extend or remove the expiry, or issue a new client.
#API_IP_NOT_ALLOWED
403 on data calls, 400 (unauthorized_client) on the token endpoint. The caller's IP isn't on the
client's IP allowlist. Fix: ask the club admin to add your server's outbound IP,
or remove the restriction if it isn't needed.
#Data call errors
#API_TOKEN_MISSING
401. No Authorization header was sent. Fix: send Authorization: Bearer <access_token> on every
data call — see Authentication.
#API_TOKEN_INVALID
401. The bearer token is unknown, expired, was killed by a secret rotation, or isn't one of ours at all
(e.g. you sent a Firebase/panel-user token by mistake — those are rejected here). Fix: fetch a fresh
token from POST /api/v1/oauth/token and retry once; if it still fails, the credential
itself needs attention.
#API_SCOPE_MISSING
403. The endpoint needs a scope your token doesn't have; params.scope names it. Fix: ask the club
admin to add the scope to the client (Settings → API access → the client → Edit), then fetch a new token.
See Scopes.
#API_PARAM_INVALID
400. A paging parameter on GET /api/v1/events was out of range; params.name is
"page" or "pageSize". Fix: page ≥ 1, pageSize 1–100.
#EVENT_NOT_FOUND
404. The event id doesn't exist, isn't this club's (main or co-host), isn't published, or is a legacy id
— all answered identically as "not found" rather than leaking which case it was. Fix: re-check the id
came from GET /api/v1/events for this same credential; don't retry with the same id.
#RACE_NOT_FOUND
404. Same rule as EVENT_NOT_FOUND, for a race id. A malformed (non-UUID) id is also a 404, not a 400.
#Cross-cutting
#RATE_LIMIT_EXCEEDED
429. Too many requests. Data calls: 60/min per client (default, may be raised per club) — never
per IP. The token endpoint: 20/min per client_id and 20/min per caller IP, whichever is hit first.
Retry-After: 60 on both — always an integer number of seconds, never an HTTP date. Fix: back off for
exactly Retry-After, with jitter if you're calling on behalf of many clients/events from one process; see
Rate limits.
#API_UNAVAILABLE
503. The read model behind /api/v1 didn't answer in time — nothing is cached for this response.
Retry-After: 30 (same integer-seconds format as 429, a shorter number because this is a different,
usually brief problem). Fix: treat exactly like RATE_LIMIT_EXCEEDED — wait and retry; this is not your
integration's fault.
#INTERNAL_ERROR
500. An unexpected server error; traceId is included. Fix: retry up to 3 attempts total, with
exponential backoff (1s, then 2s, then 4s); if it still fails, contact support with the
traceId.