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

Auth
x-api-key on every request (direct subscription). Learn more →
ICAO
4-letter airport code (e.g. KJFK). US fields often use a leading K. Learn more →

Send a GET with the airport ICAO in the path. Optional query flags control decoding.

GEThttps://data.skylinkapi.com/v3/weather/metar/{icao}
ParameterTypeRequiredDefaultDescription
icaostringYes-4-letter ICAO code — lookup, e.g. KJFK
parsedbooleanNofalseAdd 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"
}
FieldTypeRequiredDescription
rawstringYesFull METAR string as issued
icaostringYesICAO code
airport_namestringYesAirport name
timestampstring (ISO 8601)YesWhen 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
  }
}
FieldTypeRequiredDescription
parsedobjectYes (with parsed=true)Absent when parsed=false — not null
parsed.timestring (ISO 8601)YesObservation time from the METAR header — use for data age, not timestamp
parsed.wind.directionintegerYesDegrees true. 0 when calm or direction not reported
parsed.wind.speedintegerYesKnots. Never null — 0 means calm
parsed.wind.gustinteger | nullYes (key always present)Knots. null when no gust factor (not 0)
parsed.wind.variableinteger[]YesDirection range when variable, e.g. [180, 270]. [] when not variable
parsed.visibility.valuenumber | nullYes (key always present)Statute miles. null for P6SM (> 6 SM) — treat as "> 6"
parsed.visibility.reprstringYesRaw token from the METAR
parsed.cloudsarrayYesLayers lowest to highest. [] on clear sky
parsed.clouds[].typestringYes (per item)Cloud cover codes
parsed.clouds[].baseintegerYes (per item)Hundreds of feet AGL. 60 = 6,000 ft
parsed.clouds[].reprstringYes (per item)Raw token. BKN050CB = cumulonimbus at 5,000 ft
parsed.temperaturenumberYes°C
parsed.dewpointnumberYes°C
parsed.altimeternumberYesQNH setting in inHg
parsed.flight_rulesstringYesFlight category (VFR, MVFR, IFR, LIFR)
parsed.wx_codesarrayYesPresent weather. [] when none — see Present weather codes
parsed.remarksstringYesEverything after RMK in the raw string
parsed.density_altitudeintegerYesFeet — effective altitude adjusted for temperature and pressure; relevant to aircraft performance
parsed.pressure_altitudeintegerYesFeet — altitude referenced to standard atmosphere
parsed.relative_humiditynumberYes0–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).

SituationWhat you getNot this
No gust factorwind.gust: nullkey missing, or 0
Calm windwind.direction: 0, wind.speed: 0null, key missing
Wind not variablewind.variable: []null
Visibility > 6 SM (P6SM)visibility.value: null, visibility.repr: "P6"value: 0
No present weatherwx_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.

FieldTypeRequiredNull?When empty
directionintegerYesNever0 = calm or direction not specified
speedintegerYesNever0 = calm
gustinteger | nullYesYesnull when METAR has no gust factor
variableinteger[]YesNever[] when not variable; [180, 270] when 180V270 in raw

Visibility

JSON path: parsed.visibility. Always present as an object.

FieldTypeRequiredNull?When empty
valuenumber | nullYesYesnull for P6SM — treat as "> 6". Numeric otherwise
reprstringYesNeverRaw token: "10", "3", "P6", etc.

Cloud layers

JSON path: parsed.clouds[]. Always an array (possibly empty).

FieldTypeRequiredNull?Description
typestringYesNeverCloud cover codes
baseintegerYesNeverHundreds of feet AGL
reprstringYesNeverRaw 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:

FieldTypeRequiredNull?Description
reprstringYesNeverRaw token, e.g. -RA, TSRA
valuestringYesNeverExpanded 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: bool

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"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.

GET
/weather/metar/{icao}
x-api-key<token>

Your SkyLink licence key, for keys bought direct from skylinkapi.com.

In: header

Path Parameters

icao*string

4-letter ICAO airport code

Length4 <= length <= 4

Query Parameters

parsed?boolean

Include parsed/decoded METAR fields alongside raw text

Defaultfalse

Response Body

application/json

application/json

curl -X GET "https://data.skylinkapi.com/v3/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