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.
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.
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.
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.
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.
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
https://data.skyviewor.io/v1/metar?airports=ZBAA,ZSPD&mode=rawQuery parameters
| Parameter | Required | Description |
|---|---|---|
airports | Yes | Enter 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. |
mode | No | Choose 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.
{
"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
https://data.skyviewor.io/v1/metar/latest?airports=ZBAA,ZSPDAfter 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.
https://data.skyviewor.io/v1/metar/latency?airports=ZBAA,ZSPDThe 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.
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.
{
"data": [
{
"icao": "ZBAA",
"avg_latency_seconds": 52.4,
"sample_count": 120
},
{
"icao": "ZSPD",
"avg_latency_seconds": null,
"sample_count": 0
}
]
}| Field | Description |
|---|---|
icao | The ICAO code of the requested airport. |
avg_latency_seconds | Average 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_count | Number 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 airports401/403— invalid or inactive access422— invalid query format429— rate limit502/503— upstream unavailable504— 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
wss://data.skyviewor.io/v1/ws/metar?airports=ZBAA,ZSPDChange the subscription
{"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
{"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.
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 code | Airport name | City | Country |
|---|---|---|---|
ZBAA | Beijing Capital International Airport | Beijing | China |
ZBAD | Beijing Daxing International Airport | Beijing | China |
ZBDS | Ordos Ejin Horo International Airport | Ordos | China |
ZBHH | Hohhot Baita International Airport | Hohhot | China |
ZBSJ | Shijiazhuang Zhengding International Airport | Shijiazhuang | China |
ZBTJ | Tianjin Binhai International Airport | Tianjin | China |
ZBYN | Taiyuan Wusu International Airport | Taiyuan | China |
ZGDY | Zhangjiajie Hehua International Airport | Zhangjiajie | China |
ZGGG | Guangzhou Baiyun International Airport | Guangzhou | China |
ZGHA | Changsha Huanghua International Airport | Changsha | China |
ZGKL | Guilin Liangjiang International Airport | Guilin | China |
ZGNN | Nanning Wuxu International Airport | Nanning | China |
ZGOW | Jieyang Chaoshan International Airport | Jieyang | China |
ZGSZ | Shenzhen Bao'an International Airport | Shenzhen | China |
ZGZJ | Zhanjiang Wuchuan International Airport | Zhanjiang | China |
ZHCC | Zhengzhou Xinzheng International Airport | Zhengzhou | China |
ZHEC | Ezhou Huahu International Airport | Ezhou | China |
ZHHH | Wuhan Tianhe International Airport | Wuhan | China |
ZJHK | Haikou Meilan International Airport | Haikou | China |
ZJSY | Sanya Phoenix International Airport | Sanya | China |
ZLIC | Yinchuan Hedong International Airport | Yinchuan | China |
ZLLL | Lanzhou Zhongchuan International Airport | Lanzhou | China |
ZLXN | Xining Caojiabao International Airport | Xining | China |
ZLXY | Xi'an Xianyang International Airport | Xi'an / Xianyang | China |
ZLYA | Yan'an Nanniwan Airport | Yan'an | China |
ZPPP | Kunming Changshui International Airport | Kunming | China |
ZSAM | Xiamen Gaoqi International Airport | Xiamen | China |
ZSCN | Nanchang Changbei International Airport | Nanchang | China |
ZSFZ | Fuzhou Changle International Airport | Fuzhou | China |
ZSGZ | Ganzhou Huangjin Airport | Ganzhou | China |
ZSHC | Hangzhou Xiaoshan International Airport | Hangzhou | China |
ZSJN | Jinan Yaoqiang International Airport | Jinan | China |
ZSNB | Ningbo Lishe International Airport | Ningbo | China |
ZSNJ | Nanjing Lukou International Airport | Nanjing | China |
ZSNT | Nantong Xingdong International Airport | Nantong | China |
ZSOF | Hefei Xinqiao International Airport | Hefei | China |
ZSPD | Shanghai Pudong International Airport | Shanghai | China |
ZSQD | Qingdao Jiaodong International Airport | Qingdao | China |
ZSSS | Shanghai Hongqiao International Airport | Shanghai | China |
ZSWZ | Wenzhou Longwan International Airport | Wenzhou | China |
ZSYT | Yantai Penglai International Airport | Yantai | China |
ZUCK | Chongqing Jiangbei International Airport | Chongqing | China |
ZUGY | Guiyang Longdongbao International Airport | Guiyang | China |
ZULS | Lhasa Gonggar International Airport | Lhasa / Shannan | China |
ZUTF | Chengdu Tianfu International Airport | Chengdu | China |
ZUUU | Chengdu Shuangliu International Airport | Chengdu | China |
ZWSH | Kashgar Laining International Airport | Kashgar | China |
ZWTN | Hotan Airport | Hotan | China |
ZWWW | Ürümqi Tianshan International Airport | Ürümqi | China |
ZWYN | Ili Yining International Airport | Yining | China |
ZYCC | Changchun Longjia International Airport | Changchun | China |
ZYHB | Harbin Taiping International Airport | Harbin | China |
ZYQQ | Qiqihar Sanjiazi Airport | Qiqihar | China |
ZYTL | Dalian Zhoushuizi International Airport | Dalian | China |
ZYTX | Shenyang Taoxian International Airport | Shenyang | China |