Error Handling
When a request fails, the response body is JSON — but the shape depends on which layer rejected the call. Your client should branch on HTTP status first, then parse the body using the rules below. This page is the single source of truth for platform-wide error handling across all API versions (/v2, /v3, /v3.1) and all endpoints.
Auth failures differ between the two channels —
| Layer | Top-level field | Typical statuses |
|---|---|---|
| Auth / quota layer | message or code (string) | 401, 403, 429 |
| SkyLink API | detail (string or array) | 404, 422, 500, 502, 504 |
Always read Content-Type: application/json. Do not assume one error format for every status code.
Endpoint reference pages document only failures whose meaning or detail text is specific to that resource (for example METAR station missing vs TAF not issued). They link here for auth, validation, rate limits, and server errors.
Auth and quota errors
These occur before or instead of reaching the data layer. The body uses message or code, not detail.
401 — missing or invalid API key
{
"code": "missing_api_key",
"message": "Provide your key in the x-api-key header."
}The key is read from the x-api-key header only — passing it as a query parameter is not accepted and returns the same 401.
403 — endpoint not included in your plan
{
"code": "plan_forbidden",
"message": "This endpoint is not included in your plan."
}429 — abuse ceiling reached
{
"message": "Too many requests"
}See Rate Limits — on the direct API this is the per-day anti-abuse ceiling, not your monthly allowance.
JSON Schema — gateway errors (401, 403, 429)Expand section
{
"type": "object",
"required": ["message"],
"properties": {
"message": { "type": "string" }
},
"additionalProperties": true
}Fix the key or subscription — do not retry 401 or 403. Back off and retry 429; see Retry rules and Rate Limits for quota headers.
See Getting Started for header setup.
Validation errors (422)
When the request reaches SkyLink but fails input validation, detail is an array of objects:
{
"detail": [
{
"type": "string_too_short",
"loc": ["path", "icao"],
"msg": "String should have at least 4 characters",
"input": "KF",
"ctx": { "min_length": 4 }
}
]
}| Field | Description |
|---|---|
detail[].type | Validator id, e.g. string_too_short |
detail[].loc | Path to the bad field, e.g. ["path", "icao"] or ["query", "parsed"] |
detail[].msg | Human-readable explanation |
detail[].input | Value that was rejected |
detail[].ctx | Optional constraints |
JSON Schema — 422 (detail array)Expand section
{
"type": "object",
"required": ["detail"],
"properties": {
"detail": {
"type": "array",
"items": {
"type": "object",
"required": ["type", "loc", "msg", "input"],
"properties": {
"type": { "type": "string" },
"loc": { "type": "array", "items": { "type": "string" } },
"msg": { "type": "string" },
"input": { "type": "string" },
"ctx": { "type": "object" }
},
"additionalProperties": true
}
}
},
"additionalProperties": false
}Do not retry — fix the request parameters and send again.
Server and upstream errors (5xx)
When SkyLink or an upstream data service fails:
{
"detail": "Internal Server Error"
}JSON Schema — 5xx (detail string)Expand section
Same shape as business detail strings (404 uses this form too):
{
"type": "object",
"required": ["detail"],
"properties": {
"detail": { "type": "string" }
},
"additionalProperties": false
}| Status | Meaning |
|---|---|
500 | Internal server error |
502 | Upstream data service unavailable |
504 | Upstream timed out |
Retry with exponential backoff (max 3 attempts). See Retry rules.
Not found (404)
404 responses use SkyLink's string detail form, but the message and meaning are endpoint-specific — a METAR with no station, a TAF with no forecast, an unknown airport ICAO, and a missing historical flight each carry different semantics. Document 404 on each reference page (or section integration guide when the pattern spans several endpoints). Do not duplicate every variant here; link from your handler to the page that matches the endpoint you called.
| Section | Where 404 is documented |
|---|---|
| Weather — METAR / TAF | METAR · TAF · When weather data is missing |
| Weather — en-route | Winds Aloft · PIREPs · AIRMET/SIGMET |
| Airport data | Airports · Airport search |
| Reference data | Countries · Regions |
| Live ADS-B | ADS-B tracking |
| Historical ADS-B | Historical ADS-B hub · Flight detail · Position history |
| Charts | Aerodrome charts |
| Flight operations | Flight status |
| Webhooks | Subscriptions |
Important: Some endpoints return an empty result in HTTP 200 instead of 404 (for example aircraft lookup when no tail number matches). Check the reference page before mapping all "not found" cases to 404.
Plan-gate 401/403 on Historical ADS-B (subscription too low for the path prefix) are documented on the Historical ADS-B hub, not here.
Retry rules
| Status | Retry? | Action |
|---|---|---|
401, 403 | No | Fix API key / subscription |
404 | No | Endpoint-specific fallback — see reference page |
422 | No | Fix input |
429 | Yes | Exponential backoff; check quota headers |
500, 502, 504 | Yes | Exponential backoff (max 3 attempts) |
import time
import requests
RETRYABLE = {429, 500, 502, 504}
def fetch_with_retry(url: str, headers: dict, params: dict | None = None, max_retries: int = 3) -> requests.Response:
delay = 1.0
for attempt in range(max_retries + 1):
r = requests.get(url, headers=headers, params=params, timeout=(10, 15))
if r.status_code not in RETRYABLE or attempt == max_retries:
return r
time.sleep(delay)
delay *= 2
return rFor plan quotas, overage pricing, and Historical ADS-B path tiers, see Rate Limits.