MaverickWX

Storm API

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.

Fetch the active storms
curl -s https://api.maverickwx.com/api/v1/active.json
Follow a storm to its manifest
curl -s https://api.maverickwx.com/api/v1/active.json \
  | jq -r '.storms[0].manifest_url' | xargs curl -s
Check for an upstream outage before trusting an empty list
curl -s https://api.maverickwx.com/api/v1/active.json \
  | jq '{storms: (.storms|length), degraded, sources_down}'

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

GET https://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.

stormsActive 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_downWhether a source failed this run, and which. This is what separates an outage from a quiet ocean.
overviewsMulti-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.
investsAreas 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.

GET https://api.maverickwx.com/api/v1/storms/<storm_id>.json

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

GET https://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.

no-cache

GET https://api.maverickwx.com/api/v1/invests.geojson

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

GET https://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

GET https://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.

max-age=60

GET https://api.maverickwx.com/storms/<storm_id>/<issue_ts>/<product>.{png,webp,json}

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.
compactA 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_pxThe 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.

FieldMeaning
max_sustained_wind_ktCurrent max sustained wind (knots).
gust_ktCurrent gust (knots), if reported.
min_pressure_hpaMinimum central pressure (hectopascals).
movement_bearing_deg / movement_speed_ktMotion: bearing (° from north) and speed (knots). A stationary storm has speed 0 and a null bearing.
position_lat / position_lonCurrent center, signed decimal degrees (N+/S−, E+/W−).
peak_forecast_wind_ktHighest max-sustained wind across the official forecast.
has_coneWhether an official NHC cone of uncertainty is available for this storm.
wind_radiiThe 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_timesource_issue_timesource_retrieved_atpublished_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.
  • Do not use this API for any life-safety decision. For forecasts and warnings use weather.gov and the National Hurricane Center.
  • 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.