REFERENCE · METAR

METAR API

The METAR API brings aviation weather observations from Chinese airports into your application. Fetch the latest routine METAR and special SPECI reports over HTTP, or keep a WebSocket connection open to receive new reports as they arrive. Start below with a Python request and build up to a live subscription.

Quickstart

With your API key ready, retrieve the latest reports for Beijing Capital and Shanghai Pudong, then open a live subscription. Run these examples on your server or locally.

Get an API key →

1. Prepare your environment

The examples use Python 3.11 or later. Install the two dependencies below: httpx for HTTP requests and websockets for persistent connections. Use websockets 15 or later.

Shell
python -m pip install httpx "websockets>=15"

Set SKYVIEWOR_API_KEY in the environment where you run the examples, using your API key from the console. The code reads it from the environment instead of embedding it in the source. Keep the key out of repositories and browser-side code.

Shell
export SKYVIEWOR_API_KEY='YOUR_API_KEY'

2. Request the latest observation

The example requests two airports in raw mode, which returns the original report text. To work with weather fields directly, change the mode to parsed. The HTTP section below explains the parameters and response fields for both modes.

Python · HTTP
import os
import httpx

api_key = os.environ["SKYVIEWOR_API_KEY"]

with httpx.Client(timeout=15.0) as client:
    response = client.get(
        "https://data.skyviewor.io/v1/metar",
        params={"airports": "ZBAA,ZSPD", "mode": "raw"},
        headers={"Authorization": f"Bearer {api_key}"},
    )
    response.raise_for_status()
    print(response.json())

A successful request prints a JSON response. Each record in data contains an observation: icao identifies the airport and raw_metar contains the original report. Use these fields to check the airport and report text; a complete response example follows below.

3. Receive live updates

Use Python and websockets to connect with your key in the Authorization header. The example prints new reports as they arrive. Configure timeouts and reconnect with backoff for continuous operation; after reconnecting, use /v1/metar/latest to restore latest state.

Python · WebSocket
import asyncio
import json
import os
import random
from websockets.asyncio.client import connect
from websockets.exceptions import InvalidStatus

async def session():
    api_key = os.environ["SKYVIEWOR_API_KEY"]
    pong = asyncio.Event()
    async with connect(
        "wss://data.skyviewor.io/v1/ws/metar?airports=ZBAA,ZSPD",
        additional_headers={"Authorization": f"Bearer {api_key}"},
        proxy=None,
        open_timeout=15,
        ping_interval=20,
        ping_timeout=20,
        close_timeout=5,
    ) as websocket:
        print("CONNECTED: ZBAA, ZSPD", flush=True)

        async def heartbeat():
            while True:
                started = asyncio.get_running_loop().time()
                pong.clear()
                await asyncio.wait_for(websocket.send('{"action":"ping"}'), 10)
                await asyncio.wait_for(pong.wait(), 15)
                await asyncio.sleep(max(0, 20 - (asyncio.get_running_loop().time() - started)))

        async def receive():
            async for raw in websocket:
                message = json.loads(raw)
                if message.get("type") == "pong":
                    pong.set()
                elif message.get("type") == "new_metar_arrived":
                    print(message["data"], flush=True)
                elif message.get("type") == "error":
                    raise RuntimeError("Subscription/protocol error")

        tasks = [asyncio.create_task(receive()), asyncio.create_task(heartbeat())]
        try:
            done, _ = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
            for task in done:
                task.result()
        finally:
            for task in tasks:
                task.cancel()
            await asyncio.gather(*tasks, return_exceptions=True)

async def main():
    for attempt in range(8):
        try:
            await session()
            return
        except InvalidStatus as exc:
            if exc.response.status_code != 429:
                raise
            if attempt == 7:
                raise RuntimeError("WS quota still occupied: check other clients sharing this key; contact [email protected]") from None
            delay = min(20, 2 ** attempt) + random.uniform(0, 0.5)
            print(f"WS 429: quota may still be occupied; retry in {delay:.1f}s", flush=True)
            await asyncio.sleep(delay)

asyncio.run(main())

Send {"action":"ping"} every 20 seconds; the service replies {"type":"pong"}. Allow 15 seconds for a response; on timeout, close the old connection before reconnecting. After 60 seconds without an application heartbeat or valid subscription operation, the service closes with code 4000 and releases the slot. This is an inactivity timeout, not a 60-second sending interval. Protocol ping/pong (including ping_interval) does not replace the JSON heartbeat. No weather updates is normal while heartbeats remain healthy.

Keep one persistent connection and use subscribe/unsubscribe to change airports. To reconnect, cancel and await the old reader/heartbeat tasks and await ws.close() (the example uses close_timeout=5); do not run concurrent reconnect loops. Normal closure actively releases the slot, but a short release race or an abrupt network loss may leave a temporary lease and cause handshake 429. No fixed one-minute cooldown is required, and recovery within 60 seconds is not guaranteed. All clients sharing a key count toward the same 3 connections and 100 distinct airports; 429 is not necessarily an old connection. Retry serially with capped exponential backoff and jitter, then stop and show a quota error when the budget expires. This minimal example retries handshake 429 up to 8 attempts, with delays capped at 20 seconds plus up to 0.5 seconds jitter; it is not a complete long-running reconnect client. Do not retry 401/403 without correcting credentials. If the problem persists, check other clients and contact [email protected] with the UTC time and status code, never your key.

Authentication and limits

Every request needs a valid METAR API key. Send it from your server in the Authorization: Bearer skv_... header, never in the URL. Your key is not forwarded to the data source. HTTP requests share a limit of 2 QPS per key; a request for multiple airports still counts as one request.

WebSocket uses the same Authorization header with a Bearer key during the handshake. The URL specifies airports and other connection parameters, not credentials. HTTP and WebSocket can use the same key with METAR access.

Each API key supports up to 3 simultaneous WebSocket connections and 100 distinct airports across all connections. An airport selected on multiple connections counts once.

HTTP API

Use HTTP to display current airport weather or initialize your application's observation state. Queries return the latest reports for the airports you select, not a historical time range. For ongoing updates, use the WebSocket connection described in the next section.

Latest observation

GEThttps://data.skyviewor.io/v1/metar?airports=ZBAA,ZSPD&mode=raw

Query parameters

ParameterRequiredDescription
airportsYesEnter four-letter ICAO codes, separated by commas for multiple airports, such as ZBAA,ZSPD. Choose supported codes from the airport coverage section on this page.
modeNoChoose raw for the original report text (the default), or parsed for additional structured weather fields. Both modes query the latest observation, not a historical date.

Understand the response

The JSON below illustrates a raw response using a report captured during verification on October 11, 2026. It explains the response format and does not represent current weather.

json
{
  "data": [
    {
      "icao": "ZBAA",
      "raw_metar": "METAR ZBAA 101700Z 06001MPS CAVOK 13/12 Q1021 NOSIG=",
      "obs_time": "2026-10-10T17:00:00+00:00",
      "report_type": "METAR",
      "arrived_at": "2026-10-10T17:00:46+00:00"
    }
  ]
}

Match records to their airport before reading the report. obs_time is the observation time, while arrived_at is when the data source received the report. In structured mode, the response adds a parsed object with weather fields and units. Treat an empty result as unavailable observation data, not zero-valued weather.

Latest-state snapshot

GEThttps://data.skyviewor.io/v1/metar/latest?airports=ZBAA,ZSPD

After reconnecting, use this endpoint to synchronize current state. data contains the latest observations, versions identifies each airport's data version, and missing lists airports without a record. Merge the snapshot with subsequent updates and deduplicate. The snapshot does not replay every report missed while disconnected.

Latency statistics

Query the average report latency per airport over the last 72 hours. Latency runs from the report observation time, obs_time, to the time our service receives it; subsequent delivery to your client is not included. The rolling window is based on receipt time.

GEThttps://data.skyviewor.io/v1/metar/latency?airports=ZBAA,ZSPD

The required airports parameter accepts comma-separated ICAO codes, up to 100 entries. Codes are case-insensitive and duplicates are removed. Coverage, authentication and the shared 2 QPS limit per key are the same as for other HTTP queries. This endpoint does not use mode.

python
import os
import httpx

api_key = os.environ["SKYVIEWOR_API_KEY"]

with httpx.Client(timeout=15.0) as client:
    response = client.get(
        "https://data.skyviewor.io/v1/metar/latency",
        params={"airports": "ZBAA,ZSPD"},
        headers={"Authorization": f"Bearer {api_key}"},
    )
    response.raise_for_status()
    print(response.json())

The values below illustrate the response format; they are not live statistics.

json
{
  "data": [
    {
      "icao": "ZBAA",
      "avg_latency_seconds": 52.4,
      "sample_count": 120
    },
    {
      "icao": "ZSPD",
      "avg_latency_seconds": null,
      "sample_count": 0
    }
  ]
}
FieldDescription
icaoThe ICAO code of the requested airport.
avg_latency_secondsAverage latency of valid samples in the window, in seconds, rounded to one decimal place. No samples returns null; a genuine zero latency returns 0.0.
sample_countNumber of valid report samples in the window; 0 when there are none.

Only reports received in real time are sampled. Repeated copies count once; corrections count as new samples. Historical reports recovered after a disconnect, invalid timestamps and negative delays are excluded. Statistics accumulate from feature activation; no samples does not mean zero latency.

Errors

  • 400 — invalid airports
  • 401/403 — invalid or inactive access
  • 422 — invalid query format
  • 429 — rate limit
  • 502/503 — upstream unavailable
  • 504 — upstream timeout

Distinguish invalid requests from temporary failures before retrying. Check airport codes, parameters, credentials and access first. For rate limits or transient service failures, increase the retry delay and add jitter instead of sending immediate repeated requests.

WebSocket API

Keep a WebSocket connection open when your application needs to follow report updates, rather than repeatedly polling HTTP. Choose airports when connecting, then add or remove subscriptions as needed. This section covers message formats, heartbeats and recovery after a disconnect.

Connection and authentication

WSwss://data.skyviewor.io/v1/ws/metar?airports=ZBAA,ZSPD

Authentication and limits →

Change the subscription

json
{"action":"subscribe","icaos":["ZGGG"]}
{"action":"unsubscribe","icaos":["ZSPD"]}

subscribe adds airports to the current list; unsubscribe removes only the specified airports. Wildcards are not supported. The subscription_updated message confirms the resulting airport list.

Updates

json
{"type":"new_metar_arrived","data":[{"icao":"ZBAA","raw_metar":"METAR ...","obs_time":"...","report_type":"METAR","arrived_at":"..."}]}

A message like this arrives when a subscribed airport publishes a new report. Connections deliver new updates only, without initial snapshots or historical replay. To display current weather immediately, fetch a snapshot; call /v1/metar/latest again after reconnecting. Deduplicate using ICAO, obs_time and raw_metar together: corrected reports may share an observation time while containing different text.

Keep the connection healthy

Send {"action":"ping"} every 20 seconds; the service replies {"type":"pong"}. Allow 15 seconds for a response; on timeout, close the old connection before reconnecting. After 60 seconds without an application heartbeat or valid subscription operation, the service closes with code 4000 and releases the slot. This is an inactivity timeout, not a 60-second sending interval. Protocol ping/pong (including ping_interval) does not replace the JSON heartbeat. No weather updates is normal while heartbeats remain healthy.

Recover after a disconnect

4000 means the application heartbeat timed out: check the heartbeat task and event loop, reconnect with backoff, restore subscriptions and synchronize /v1/metar/latest. Connections last up to 24 hours and close with 4008 at expiry. Temporary delivery or verification failures may close with 1013. Reconnect using exponential backoff with jitter, restore your subscriptions and synchronize a fresh snapshot. A 4403 close means the key was revoked, rotated or no longer has active access: check credentials before retrying indefinitely. A lack of new weather reports alone does not indicate a failed connection.

Keep one persistent connection and use subscribe/unsubscribe to change airports. To reconnect, cancel and await the old reader/heartbeat tasks and await ws.close() (the example uses close_timeout=5); do not run concurrent reconnect loops. Normal closure actively releases the slot, but a short release race or an abrupt network loss may leave a temporary lease and cause handshake 429. No fixed one-minute cooldown is required, and recovery within 60 seconds is not guaranteed. All clients sharing a key count toward the same 3 connections and 100 distinct airports; 429 is not necessarily an old connection. Retry serially with capped exponential backoff and jitter, then stop and show a quota error when the budget expires. This minimal example retries handshake 429 up to 8 attempts, with delays capped at 20 seconds plus up to 0.5 seconds jitter; it is not a complete long-running reconnect client. Do not retry 401/403 without correcting credentials. If the problem persists, check other clients and contact [email protected] with the UTC time and status code, never your key.

Latest-state snapshot →
Report delivery

A successful server send does not confirm that your application processed or saved the report. Handle consumption and persistence in your application. Snapshots restore latest state after a disconnect, but the API does not guarantee replay of every historical report.

Airport coverage

Choose an airport from the table before querying or subscribing. Use its ICAO code in the HTTP airports parameter or the WebSocket icaos array. These airports are supported, but the availability of an observation at a particular time depends on reporting by the airport.

Supported airports: 55

ICAO codeAirport nameCityCountry
ZBAABeijing Capital International AirportBeijingChina
ZBADBeijing Daxing International AirportBeijingChina
ZBDSOrdos Ejin Horo International AirportOrdosChina
ZBHHHohhot Baita International AirportHohhotChina
ZBSJShijiazhuang Zhengding International AirportShijiazhuangChina
ZBTJTianjin Binhai International AirportTianjinChina
ZBYNTaiyuan Wusu International AirportTaiyuanChina
ZGDYZhangjiajie Hehua International AirportZhangjiajieChina
ZGGGGuangzhou Baiyun International AirportGuangzhouChina
ZGHAChangsha Huanghua International AirportChangshaChina
ZGKLGuilin Liangjiang International AirportGuilinChina
ZGNNNanning Wuxu International AirportNanningChina
ZGOWJieyang Chaoshan International AirportJieyangChina
ZGSZShenzhen Bao'an International AirportShenzhenChina
ZGZJZhanjiang Wuchuan International AirportZhanjiangChina
ZHCCZhengzhou Xinzheng International AirportZhengzhouChina
ZHECEzhou Huahu International AirportEzhouChina
ZHHHWuhan Tianhe International AirportWuhanChina
ZJHKHaikou Meilan International AirportHaikouChina
ZJSYSanya Phoenix International AirportSanyaChina
ZLICYinchuan Hedong International AirportYinchuanChina
ZLLLLanzhou Zhongchuan International AirportLanzhouChina
ZLXNXining Caojiabao International AirportXiningChina
ZLXYXi'an Xianyang International AirportXi'an / XianyangChina
ZLYAYan'an Nanniwan AirportYan'anChina
ZPPPKunming Changshui International AirportKunmingChina
ZSAMXiamen Gaoqi International AirportXiamenChina
ZSCNNanchang Changbei International AirportNanchangChina
ZSFZFuzhou Changle International AirportFuzhouChina
ZSGZGanzhou Huangjin AirportGanzhouChina
ZSHCHangzhou Xiaoshan International AirportHangzhouChina
ZSJNJinan Yaoqiang International AirportJinanChina
ZSNBNingbo Lishe International AirportNingboChina
ZSNJNanjing Lukou International AirportNanjingChina
ZSNTNantong Xingdong International AirportNantongChina
ZSOFHefei Xinqiao International AirportHefeiChina
ZSPDShanghai Pudong International AirportShanghaiChina
ZSQDQingdao Jiaodong International AirportQingdaoChina
ZSSSShanghai Hongqiao International AirportShanghaiChina
ZSWZWenzhou Longwan International AirportWenzhouChina
ZSYTYantai Penglai International AirportYantaiChina
ZUCKChongqing Jiangbei International AirportChongqingChina
ZUGYGuiyang Longdongbao International AirportGuiyangChina
ZULSLhasa Gonggar International AirportLhasa / ShannanChina
ZUTFChengdu Tianfu International AirportChengduChina
ZUUUChengdu Shuangliu International AirportChengduChina
ZWSHKashgar Laining International AirportKashgarChina
ZWTNHotan AirportHotanChina
ZWWWÜrümqi Tianshan International AirportÜrümqiChina
ZWYNIli Yining International AirportYiningChina
ZYCCChangchun Longjia International AirportChangchunChina
ZYHBHarbin Taiping International AirportHarbinChina
ZYQQQiqihar Sanjiazi AirportQiqiharChina
ZYTLDalian Zhoushuizi International AirportDalianChina
ZYTXShenyang Taoxian International AirportShenyangChina