METAR
METARs (Meteorological Aerodrome Reports) are surface weather observations taken at a specific airfield or station. Standard METARs are issued at regular intervals (typically hourly or every half‑hour), while a special report (SPECI) is released when conditions at a staffed site change significantly. Each METAR includes key meteorological data such as temperature, wind speed and direction, visibility, cloud cover, and atmospheric pressure. Pilots, air traffic controllers, dispatchers, and meteorologists use these reports to evaluate airport weather, supporting flight planning, navigation, and operational decisions.
Request
Requirements
x-api-key on every request (direct subscription). Learn more →Send a GET with the airport ICAO in the path. Optional query flags control decoding.
https://data.skylinkapi.com/v3.1/weather/metar/{icao}| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
icao | string | Yes | - | 4-letter ICAO code — lookup, e.g. KJFK |
parsed | boolean | No | false | Add decoded fields under a parsed key |
Response
Raw (default)
{
"raw": "METAR KJFK 062251Z 24017G23KT 10SM FEW060 FEW075 SCT110 BKN180 BKN250 28/17 A2974 RMK AO2 SLP072 T02830167 $",
"icao": "KJFK",
"airport_name": "John F. Kennedy International Airport",
"timestamp": "2026-06-06T23:09:37Z"
}| Field | Type | Required | Description |
|---|---|---|---|
raw | string | Yes | Full METAR string as issued |
icao | string | Yes | ICAO code |
airport_name | string | Yes | Airport name |
timestamp | string (ISO 8601) | Yes | When the API fetched this data — not the observation time |
Parsed (parsed=true)
{
"raw": "METAR KJFK 062251Z 24017G23KT 10SM FEW060 FEW075 SCT110 BKN180 BKN250 28/17 A2974 RMK AO2 SLP072 T02830167 $",
"icao": "KJFK",
"airport_name": "John F. Kennedy International Airport",
"timestamp": "2026-06-06T23:09:37Z",
"parsed": {
"time": "2026-06-07T00:51:00+02:00",
"wind": {
"direction": 240,
"speed": 17,
"gust": 23,
"variable": []
},
"visibility": {
"value": 10,
"repr": "10"
},
"clouds": [
{ "type": "FEW", "base": 60, "repr": "FEW060" },
{ "type": "FEW", "base": 75, "repr": "FEW075" },
{ "type": "SCT", "base": 110, "repr": "SCT110" },
{ "type": "BKN", "base": 180, "repr": "BKN180" },
{ "type": "BKN", "base": 250, "repr": "BKN250" }
],
"temperature": 28,
"dewpoint": 17,
"altimeter": 29.74,
"flight_rules": "VFR",
"wx_codes": [],
"remarks": "RMK AO2 SLP072 T02830167 $",
"density_altitude": 1792,
"pressure_altitude": 193,
"relative_humidity": 0.49
}
}| Field | Type | Required | Description |
|---|---|---|---|
parsed | object | Yes (with parsed=true) | Absent when parsed=false — not null |
parsed.time | string (ISO 8601) | Yes | Observation time from the METAR header — use for data age, not timestamp |
parsed.wind.direction | integer | Yes | Degrees true. 0 when calm or direction not reported |
parsed.wind.speed | integer | Yes | Knots. Never null — 0 means calm |
parsed.wind.gust | integer | null | Yes (key always present) | Knots. null when no gust factor (not 0) |
parsed.wind.variable | integer[] | Yes | Direction range when variable, e.g. [180, 270]. [] when not variable |
parsed.visibility.value | number | null | Yes (key always present) | Statute miles. null for P6SM (> 6 SM) — treat as "> 6" |
parsed.visibility.repr | string | Yes | Raw token from the METAR |
parsed.clouds | array | Yes | Layers lowest to highest. [] on clear sky |
parsed.clouds[].type | string | Yes (per item) | Cloud cover codes |
parsed.clouds[].base | integer | Yes (per item) | Hundreds of feet AGL. 60 = 6,000 ft |
parsed.clouds[].repr | string | Yes (per item) | Raw token. BKN050CB = cumulonimbus at 5,000 ft |
parsed.temperature | number | Yes | °C |
parsed.dewpoint | number | Yes | °C |
parsed.altimeter | number | Yes | QNH setting in inHg |
parsed.flight_rules | string | Yes | Flight category (VFR, MVFR, IFR, LIFR) |
parsed.wx_codes | array | Yes | Present weather. [] when none — see Present weather codes |
parsed.remarks | string | Yes | Everything after RMK in the raw string |
parsed.density_altitude | integer | Yes | Feet — effective altitude adjusted for temperature and pressure; relevant to aircraft performance |
parsed.pressure_altitude | integer | Yes | Feet — altitude referenced to standard atmosphere |
parsed.relative_humidity | number | Yes | 0–1 decimal |
Object schemas and nullability
With parsed=true, the parsed object always includes every key — nothing is omitted when data is missing. Use null, 0, or [] depending on the field. Without parsed=true, the parsed key is absent (not null).
| Situation | What you get | Not this |
|---|---|---|
| No gust factor | wind.gust: null | key missing, or 0 |
| Calm wind | wind.direction: 0, wind.speed: 0 | null, key missing |
| Wind not variable | wind.variable: [] | null |
Visibility > 6 SM (P6SM) | visibility.value: null, visibility.repr: "P6" | value: 0 |
| No present weather | wx_codes: [] | null, key missing |
Clear sky (SKC/CLR) | clouds: [] | null, key missing |
Nested object schemas (wind, visibility, clouds, present weather)Expand section
Wind
JSON path: parsed.wind. Always present as an object.
| Field | Type | Required | Null? | When empty |
|---|---|---|---|---|
direction | integer | Yes | Never | 0 = calm or direction not specified |
speed | integer | Yes | Never | 0 = calm |
gust | integer | null | Yes | Yes | null when METAR has no gust factor |
variable | integer[] | Yes | Never | [] when not variable; [180, 270] when 180V270 in raw |
Visibility
JSON path: parsed.visibility. Always present as an object.
| Field | Type | Required | Null? | When empty |
|---|---|---|---|---|
value | number | null | Yes | Yes | null for P6SM — treat as "> 6". Numeric otherwise |
repr | string | Yes | Never | Raw token: "10", "3", "P6", etc. |
Cloud layers
JSON path: parsed.clouds[]. Always an array (possibly empty).
| Field | Type | Required | Null? | Description |
|---|---|---|---|---|
type | string | Yes | Never | Cloud cover codes |
base | integer | Yes | Never | Hundreds of feet AGL |
repr | string | Yes | Never | Raw token, e.g. BKN050CB |
Present weather codes
JSON path: parsed.wx_codes[]. Always an array (possibly empty). When present weather is reported, each element has both keys:
| Field | Type | Required | Null? | Description |
|---|---|---|---|---|
repr | string | Yes | Never | Raw token, e.g. -RA, TSRA |
value | string | Yes | Never | Expanded text, e.g. Light Rain, Thunderstorm Rain |
Example from live API (KORD):
"wx_codes": [{ "repr": "-RA", "value": "Light Rain" }]Client types
Copy into your project. Nullability matches Object schemas and nullability above.
"""METAR response and summary types - matches weather-metar field tables."""from __future__ import annotationsfrom dataclasses import dataclassfrom typing import LiteralFlightRules = Literal["VFR", "MVFR", "IFR", "LIFR"]@dataclassclass MetarRequest: """Path/query input for GET /weather/metar/{icao}.""" icao: str parsed: bool = False@dataclassclass MetarWind: direction: int speed: int gust: int | None variable: list[int]@dataclassclass MetarVisibility: value: float | None repr: str@dataclassclass MetarCloudLayer: type: str base: int repr: str@dataclassclass MetarWxCode: repr: str value: str@dataclassclass MetarParsed: time: str wind: MetarWind visibility: MetarVisibility clouds: list[MetarCloudLayer] temperature: float dewpoint: float altimeter: float flight_rules: FlightRules wx_codes: list[MetarWxCode] remarks: str density_altitude: int pressure_altitude: int relative_humidity: float@dataclassclass MetarResponse: raw: str icao: str airport_name: str timestamp: str parsed: MetarParsed | None = None@dataclassclass MetarSummary: """Shape returned by metar_summary() in the fetch example.""" icao: str category: FlightRules wind_kt: int gust_kt: int | None visibility_display: str age_min: float stale: boolWire types (MetarResponse, MetarParsed) mirror JSON from parsed=true. MetarSummary is defined at the top of each tab in the code example.
Integration
Fetch, cache, and summarize observations — the step after you parse a successful response.
The example caches each METAR for five minutes (observations update at most every 30–60 minutes), uses 10 s / 15 s connect/read timeouts, and builds a summary from parsed.time (observation time), not timestamp (API fetch time).
import osimport timefrom dataclasses import dataclassfrom datetime import datetime, timezonefrom typing import Literalimport requests# MetarSummary - helper return type. Full wire types (MetarResponse, MetarParsed, …)# are in the Client types section on the METAR docs page.FlightRules = Literal["VFR", "MVFR", "IFR", "LIFR"]@dataclassclass MetarSummary: icao: str category: FlightRules wind_kt: int gust_kt: int | None visibility_display: str age_min: float stale: boolHEADERS = { "x-api-key": os.getenv("SKYLINK_API_KEY", "YOUR_API_KEY")}BASE = "https://data.skylinkapi.com/v3.1"CACHE: dict[str, tuple[dict, float]] = {}METAR_TTL = 300 # 5 min - safe inside the 30–60 min observation cycledef parse_observation_time(iso: str) -> datetime: """Parse parsed.time - API returns ISO 8601 with timezone offset.""" return datetime.fromisoformat(iso.replace("Z", "+00:00"))def fetch_metar(icao: str) -> dict | None: cache_key = f"metar:{icao}:parsed" cached = CACHE.get(cache_key) if cached and time.time() < cached[1]: return cached[0] try: r = requests.get( f"{BASE}/weather/metar/{icao}", headers=HEADERS, params={"parsed": "true"}, timeout=(10, 15), ) except requests.Timeout as exc: raise RuntimeError(f"SkyLink timeout fetching METAR for {icao}") from exc if r.status_code == 404: return None r.raise_for_status() data = r.json() CACHE[cache_key] = (data, time.time() + METAR_TTL) return datadef metar_summary(icao: str) -> MetarSummary | None: metar = fetch_metar(icao) if not metar: return None p = metar["parsed"] obs = parse_observation_time(p["time"]) age_min = (datetime.now(timezone.utc) - obs).total_seconds() / 60 vis = p["visibility"]["value"] return MetarSummary( icao=icao, category=p["flight_rules"], wind_kt=p["wind"]["speed"], gust_kt=p["wind"]["gust"], visibility_display=f"{vis} SM" if vis is not None else "> 6 SM", age_min=round(age_min, 1), stale=age_min > 90, )if __name__ == "__main__": import json from dataclasses import asdict summary = metar_summary("KJFK") print(json.dumps(asdict(summary), indent=2) if summary else "METAR not available")More examples: Weather use cases.
Implementation notes
timestamp vs parsed.time. timestamp changes on every request. parsed.time is the observation time from the METAR header — use it for "data is X minutes old."
visibility.value being null. When the station reports P6SM, value is null and repr is "P6". Treat null as "> 6 SM", not missing data.
Station availability. Not every ICAO has a METAR station. A 404 means no observation is available — the airport may still exist. Show an empty state, not an error banner.
Caching. A 3–5 minute client cache cuts duplicate calls without missing new observations.
Error responses
Only METAR-specific failures belong here. For auth, validation, rate limits, and server errors, see Error Handling. For METAR vs TAF fallback patterns, see When weather data is missing.
404 — no METAR for this ICAO
{
"detail": "Airport not found for ICAO code: KXYZ"
}The airport may not exist, or it exists but has no METAR station. The message does not distinguish these cases — treat any METAR 404 as no observation available.
Your SkyLink licence key, for keys bought direct from skylinkapi.com.
In: header
Path Parameters
4-letter ICAO airport code
4 <= length <= 4Query Parameters
Include parsed/decoded METAR fields alongside raw text
falseResponse Body
application/json
application/json
curl -X GET "https://data.skylinkapi.com/v3.1/weather/metar/KJFK"{
"raw": "METAR KJFK 271851Z 16013KT 10SM FEW024 15/09 A3020 RMK AO2 SLP216 T01500089",
"icao": "KJFK",
"airport_name": "John F Kennedy International Airport",
"timestamp": "2025-09-27T12:00:00Z"
}{
"detail": [
{
"loc": [
"string"
],
"msg": "string",
"type": "string",
"input": null,
"ctx": {}
}
]
}Related: TAF · Use cases · Errors · Production