The tropical-cyclone JSON and image contract: static objects, fetched with a
plain GET.
There is no application server. The API is a small set of static JSON
objects, served straight from object storage behind a CDN. Fetch them with a plain
GET — no key, no signup, no rate limit to negotiate. A machine-readable
schema is at /schema/api-v1.json.
Base URL
https://api.maverickwx.com
Every path below is relative to this. Product and image URLs inside the manifests
are already absolute — use them as given rather than rebuilding them.
Your first three requests
Three requests, in the order a client should make them. Paste the first one; it works
as written.
Read that third one before you ship. An empty storms
array does not mean the tropics are quiet — it can also mean a source is down. See
Outage is not "no storms".
Endpoints
GEThttps://api.maverickwx.com/api/v1/active.json
The entry point. The global list of active storms. Written last
in every publish run, so it never references a storm whose objects do not yet
exist.
storms
Active cyclones. Each carries a manifest_url to follow. A storm leaves this list the moment its agency issues its last product on it (JTWC’s final warning, NHC’s last advisory); it stays published, and is found through archive.json or its own manifest.
degraded, sources_down
Whether a source failed this run, and which. This is what separates an outage from a quiet ocean.
overviews
Multi-storm overview maps — a global frame plus one per active basin, each with region, label, png_url/webp_url and pixel dimensions. Since 1.3 each also carries renditions and compact.
invests
Areas of possible formation, listed separately from storms: invest_id, basin, source, position, and formation probability and risk at each horizon (prob_2day/prob_7day, risk_2day/risk_7day). An invest may carry guidance — the genesis model-guidance graphic — or null where there is none we can honestly attribute.
no-cache Revalidate with the ETag; a 304 costs nothing.
One storm. Its current issuance, the URLs of its rendered graphics (including the social-preview
card), and the canonical snapshot. Do not build this URL yourself — take
manifest_url from active.json.
Carries a compact summary so a client can show
intensity without downloading the full snapshot.
final_advisory is true when the issuing centre has said this
is its last product on the system. Check it before judging
source_issue_time against source_stale_after_seconds: on an
ended storm that age grows without bound and means nothing, because nothing further
is coming.
no-cache
GEThttps://api.maverickwx.com/api/v1/archive.json
Permanent storm archive. A retained index of active and completed
storms. Each row carries the immutable storm_id, current/final manifest
URL, active state, first/last-seen timestamps and (after completion)
archived_at. Use https://maverickwx.com/storms/<storm_id>
as the stable human-facing URL.
Formation-outlook areas as a GeoJSON FeatureCollection
— one Polygon per invest that has one, with invest_id,
basin, source, the per-horizon probabilities and risks,
and observed_time. Ring coordinates are [lon, lat] and the
ring is closed. It is its own object rather than a field on active.json
because NHC samples each ring at roughly 300 points.
Only NHC areas appear here. The Graphical Tropical Weather Outlook
publishes a polygon; JTWC's ABPW publishes a point and a coarse potential and
nothing more. A JTWC invest is listed in active.json with its point and
has no feature here. Nothing is buffered into an area the source did not draw.
An empty features array means a quiet outlook, and is published rather
than skipped.
no-cache
GEThttps://api.maverickwx.com/api/v1/status.json
Pipeline health. Generated time, active count, degraded flag. Small
enough to poll if you are monitoring rather than consuming.
no-cache
GEThttps://api.maverickwx.com/api/v1/notices.json
Maintenance notices. Operator messages the site shows as a banner —
planned maintenance, a degraded feed, an outage. Each carries a kind,
title, body, scope and an optional
starts_at/ends_at window. The window is evaluated at read
time, so a notice appears and disappears on its own schedule without a republish.
The object always exists and carries generated_at, with
"notices": [] when there are none — so "nothing to report" and "could not
fetch" are different observable states rather than the same silence.
The graphics themselves — PNG master, WebP, and the provenance
sidecar. Keyed by issuance time and never overwritten, so a URL you hold is the
bytes you saw. There is no latest/ alias: the manifest is the pointer,
which is why an image URL is never stale.
Smaller deliveries of the same graphic are filed beside it as
<product>@640w.webp, and the phone redraw as
<product>-compact.webp with rungs of its own. Every one is a
product in its own right, with a sidecar and a hash. Read them from
renditions rather than building the
names — the widths are ours to change.
immutable, 1 year Cache these as hard as you like.
Sizes, and the graphic drawn for a phone
Every graphic is published at several widths. png_url,
webp_url, width_px and height_px still name the
2400 px master and always will; renditions lists every delivered
size, ascending, with the master as the last entry.
renditions[]
The same picture, resampled. Each carries width_px/height_px, png_url/webp_url, a sidecar_url and its own image_sha256. Feed them to srcset with width descriptors.
compact
A different picture of the same data, redrawn at a frame a phone can read: larger type, and the map key, place names, forecast-point times and half the stats strip taken out. It has its own graphic_type and its own renditions, or is null where there is none.
min_label_px
The type size, in that rendition’s own pixels, of the smallest label a reader has to read. See below.
Do not put compact in the same srcset as
renditions. A browser choosing on width alone would serve the
abbreviated picture to a desktop that has room for the whole one. It belongs behind a
<source media> — art direction, not resolution.
Deciding whether a size is readable
A raster’s labels are baked in, so their size on screen is fixed by the box you
draw the image into — and nothing on your side can measure them.
min_label_px is the half of that calculation only the renderer knows: the
smallest label clears a floor of F CSS pixels exactly when
min_label_px × css_width ÷ width_px ≥ F
It scales with the rendition, because resampling scales the labels too — which is why
no rung of the ladder can rescue a desktop master on a phone, and why the
compact graphic is drawn rather than resized. This site uses a floor of
12 CSS px and switches to the compact graphic below 640 px. A value of
0 means the graphic declared nothing; read it as unknown, not as
illegible.
The summary object
Each per-storm manifest carries a summary denormalized from the canonical
snapshot, so a reader can show intensity and motion without downloading the full
snapshot. Values are raw canonical units; presentation — mph, Saffir–Simpson category,
compass points — is the reader's job. Any field is null when the source
omits it, and null never means zero.
Field
Meaning
max_sustained_wind_kt
Current max sustained wind (knots).
gust_kt
Current gust (knots), if reported.
min_pressure_hpa
Minimum central pressure (hectopascals).
movement_bearing_deg / movement_speed_kt
Motion: bearing (° from north) and speed (knots). A stationary storm has speed 0 and a null bearing.
position_lat / position_lon
Current center, signed decimal degrees (N+/S−, E+/W−).
peak_forecast_wind_kt
Highest max-sustained wind across the official forecast.
has_cone
Whether an official NHC cone of uncertainty is available for this storm.
wind_radii
The current fix's wind-field extent, one entry per threshold the source reported: threshold_kt (34, 50 or 64) with ne_nm / se_nm / sw_nm / nw_nm in nautical miles. Since schema 1.4. An empty array means the source reported no radii — it is not an unknown extent to be filled in, and a quadrant of null means that direction was not reported, which is not zero.
The two clocks, and the forecast timeline
A storm manifest carries two different issuance times and they are not
interchangeable. source_issue_time is when the centre issued the advisory or
warning the current position and winds come from. forecast_init_time is when
the forecast was initialized — the cycle each point's tau counts
from.
On NHC and CPHC storms the forecast cycle is 3 to 6 hours earlier than the
advisory, always. NHC initializes on the synoptic hour and issues the advisory built on it
up to six hours later, carrying that advisory's own position as the cycle's
tau = 3 point. So a forecast point's lead time measured from
source_issue_time is not its tau, and comes out negative
for the early points. Use tau, or measure from
forecast_init_time. On JTWC storms the two coincide.
advisory_number is the centre's own number for that issuance — NHC's
"Advisory 4" (or "4A" for an intermediate, hence a string) and JTWC's warning
number — so Advisory 4 · 27 Aug 00:00Z is answerable from the manifest alone.
It is null when the issuance stated no number we could read; show that as an
absence, not a blank.
Each forecast point's status comes with a status_origin and the
two are read together: stated (the centre's classification),
derived (the centre gave none, so it was computed from
max_sustained_wind_kt — show it, but say it is derived), or
unclassified (neither; status is null). A
status of "unknown" means the centre classified the point in
terms this schema does not recognize, and should not be shown to a reader by default. The
status values are the canonical enum — machine tokens like
tropical_storm, not prose to print.
Four provenance clocks, in the order they happen: forecast_init_time →
source_issue_time → source_retrieved_at →
published_at. updated_at is a fifth and a different kind: it is
when the pipeline run started, identical across every storm in that run, and is
the field to judge against stale_after_seconds. Retrieval can read a little
earlier than issuance on NHC storms — the centre posts a product a few minutes
before the hour it is stamped with, and both times are reported as measured.
retained: true means this storm's last complete issuance is being held rather
than replaced: a newer current state arrived without a forecast cycle of its own, and
pairing them would have shown a current position beside a superseded forecast.
retained_reason says which. Everything else in the manifest still describes
that complete issuance and is still current data.
Outage is not "no storms"
An empty storms array with degraded: false means genuinely no
active cyclones. degraded: true — or a failed fetch — means data is delayed
while a source recovers. These are different states and conflating them is the
one mistake this API is most likely to lead you into, because both look like an
empty list if you only check length.
Units, attribution and limits
Wind is in knots, pressure in hectopascals, coordinates in signed decimal degrees.
Longitude is normalized consistently across sources, including across the dateline.
Attribution ("Data: NOAA/NHC" / "Data: JTWC") must be shown.
These graphics are not an official government product, and must not
be presented as one.
The Radar page is not part of this
contract. It is a live viewer: the browser reads third-party radar and satellite
services directly, and none of that data passes through this API.
Versioning
Every object carries schema_version, currently 1.5.
Within a major version the contract only ever gains fields: nothing you
already parse changes shape, disappears, or changes meaning. Ignore fields you do not
recognise and you will not be broken by a minor release.
1.3 added renditions and compact to each published graphic.
A client written against 1.2 keeps reading png_url,
webp_url, width_px and height_px, and keeps
getting the 2400 px master exactly as before.
1.5 added run_id and run_finished_at to
status.json. They name the pipeline run that produced it, which is
otherwise unnamed: generated_at is when the run started, and
the difference between the two is how long it took. Both are null on a
status written by anything other than a full pipeline run.
A field being removed or repurposed would be a major version, served alongside this
one rather than in place of it. Product URLs are immutable and cached for a year, so
an address you hold today keeps resolving to the bytes you saw whatever happens to the
schema around it.