JSON API v1
Plain static files on a CDN — no server, no keys, no rate limit.
Access-Control-Allow-Origin: * and
Cache-Control: public, max-age=3600, stale-while-revalidate=86400 on everything under
/api/. Every response carries schema_version, generated_at,
data_through, window, units, method and
truth; every result row can be turned into a permanent link.
Endpoints
| Path | Contents |
|---|---|
scores/latest.json |
every published aggregate, compact {columns, rows} encoding. |
scores/leaderboard.json |
the station_id=ALL slice only. |
leaderboard/{window}-{init}z-{method}-{variable}.json |
one pre-built file per site view, ranked, with a permalink per row. |
scores/{station}/{model}/{lead}.json |
one permanent-link card: every window/init/method/variable, the pairwise table and the daily error series. |
pairwise/latest.json |
paired model-vs-model MAE differences (station_id=ALL). |
stations.json | station metadata. |
models.json | model registry incl. the persistence baseline. |
status.json | pipeline completeness report. |
openapi.json | OpenAPI 3.1 description of the above. |
curl
# the front-page ranking, straight to a table curl -s https://castcheck.zifanzhang.com/api/v1/leaderboard/90d-00z-bilinear-tmax.json \ | jq -r '.results[] | [.rank, .model_id, .mae, .bias, .n] | @tsv' # one permanent-link card curl -s https://castcheck.zifanzhang.com/api/v1/scores/ALL/gfs/1.json | jq '.scores.columns' # is anything missing today? curl -s https://castcheck.zifanzhang.com/api/v1/status.json | jq '.ok, .n_current_gaps'
Python
import io, urllib.request, json
import pandas as pd
BASE = "https://castcheck.zifanzhang.com/api/v1"
def table(path):
"""Read a compact {columns, rows} endpoint into a DataFrame."""
with urllib.request.urlopen(f"{BASE}/{path}") as r:
payload = json.load(r)
df = pd.DataFrame(payload["rows"], columns=payload["columns"])
df.attrs.update({k: payload[k] for k in
("generated_at", "data_through", "units", "method", "truth")})
return df
scores = table("scores/latest.json")
best = (scores.query("station_id == 'ALL' and window == '90d' and init_hour == 0 "
"and method == 'bilinear' and variable == 'tmax' and lead_day == 1 "
"and n >= 30")
.sort_values("mae"))
print(best[["model_id", "n", "mae", "mae_ci_low", "mae_ci_high", "bias"]])
# the full per-day record (°C), ~one row per station/model/init/lead/method/variable/day
errors = pd.read_csv("https://castcheck.zifanzhang.com/data/daily_errors.csv.gz")
MAE, bias, RMSE and the interval bounds are in °C in the API and the CSVs; the site displays them in °F (multiply a difference by 1.8).
Response envelope
{
"schema_version": "0.1",
"methodology_version": "0.2",
"castcheck_version": "0.1.0",
"generated_at": "…UTC ISO-8601…",
"data_through": "2026-08-30",
"next_update": "2026-08-31T11:00:00+00:00",
"window": {"type": "90d", "days": 90, "start": "…", "end": "…"},
"units": {"mae": "degC", "bias": "degC", "rmse": "degC", "n": "days",
"hit1f": "fraction", "skill_persistence": "fraction"},
"method": {"ci": "moving-block bootstrap", "resamples": 1000, "level": 0.95,
"block": "7 days", "ref": "https://castcheck.zifanzhang.com/methodology/"},
"truth": {"source": "NWS Daily Climate Report (CLI), first final issuance"},
"license": "CC-BY-4.0",
"columns": [...], "rows": [[...]]
}
Stability
Paths under /api/v1/ and the permanent links
/station/{ICAO}/model/{model_id}/lead/{d}/ are fixed. Columns may be
added within v1; a removal or a change of meaning bumps schema_version and
appears in the changelog. Numbers change every day as history
accumulates — always record generated_at and data_through with anything
you quote.