Every error response has the shape:
{
"error": "code",
"error_description": "human-readable explanation"
}
The error value is a stable string — your code can match against
it without parsing English prose. Descriptions are best-effort and
may evolve.
HTTP status reference
| Status | Meaning | Typical error |
|---|---|---|
400 Bad Request | Your request was malformed | invalid_request |
401 Unauthorized | Missing or invalid credentials | invalid_token / invalid_client |
403 Forbidden | Authenticated, but not allowed | insufficient_scope, unauthorized_client |
404 Not Found | Resource doesn't exist (or doesn't belong to you) | not_found |
413 Payload Too Large | IGC body over 3 MB | payload_too_large |
422 Unprocessable Entity | IGC failed pipeline parsing | ingest_failed |
429 Too Many Requests | Rate limit exceeded | (no error code — see Retry-After) |
500 Internal Server Error | Bug on our side | server_error |
OAuth-specific errors
These follow RFC 6749 §5.2. They appear in 4xx responses from
/api/oauth/token, /api/oauth/revoke, and /api/oauth/introspect.
| Code | Meaning |
|---|---|
invalid_request | Required parameter missing or malformed |
invalid_client | Client authentication failed (bad client_id/secret, or wrong auth method) |
invalid_grant | The code/refresh_token is expired, already used, revoked, or doesn't match redirect_uri/PKCE |
unauthorized_client | Your client isn't allowed to use this grant type, or your app is suspended |
unsupported_grant_type | We don't support that grant_type (we accept client_credentials, authorization_code, refresh_token) |
invalid_scope | A requested scope is unknown or not allowed for your app |
access_denied | The user clicked "Cancel" on the consent screen |
Device-code grant (RFC 8628 §3.5)
Returned from POST /api/oauth/token with grant_type=urn:ietf:params:oauth:grant-type:device_code:
| Code | Meaning | What to do |
|---|---|---|
authorization_pending | The user has not yet entered the user_code | Keep polling at the advertised interval |
slow_down | You're polling faster than the advertised interval | Add 5 seconds to your interval, keep polling |
expired_token | The 10-minute code lifetime has elapsed | Restart the flow at /api/oauth/device_authorization |
access_denied | The user clicked "Cancel" | Surface to the user; don't restart automatically |
Bearer-token errors (API v1)
These appear with a WWW-Authenticate header per RFC 6750 §3.
WWW-Authenticate: Bearer error="insufficient_scope", error_description="…", scope="flights:write"
| Code | Meaning | What to do |
|---|---|---|
invalid_request | Missing or malformed Authorization header | Fix the request |
invalid_token | Token unknown, expired, or revoked | Refresh (OAuth) or re-issue (API key) |
insufficient_scope | Token doesn't carry the required scope | Re-authorise with the right scope |
Rate-limit errors
429 Too Many Requests is returned with:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1748426400
{
"success": false,
"error": "Rate limit exceeded. Please try again later.",
"retryAfter": 12
}
Sleep Retry-After seconds before retrying. Permanent loops will get
your app suspended.
Theft-detection cascade
If you present a refresh token that has already been rotated, all
of your tokens for that user are revoked — RFC 6819 §5.2.2.3. You
will receive invalid_grant immediately, and subsequent API calls
with previously-issued access tokens will return invalid_token. The
user must re-authorise from scratch.
This is non-overridable. Treat refresh tokens as single-use and atomically replace your stored copy with the new one before any retries.