API guide
Generated from docs/API.md
A small keyed HTTP API for third-party developers: derived marine-forecast answers — a
point forecast, model agreement, the automatic model choice, tropical-cyclone tracks and
US federal buoy observations — computed from the published forecast releases by the same
code the website runs (Frontend/core.js, ADR-32, ADR-33). The machine-readable spec is
api/openapi.yaml, served at https://api.predictsea.com/v1/openapi.yaml.
The terms are the API terms (https://www.predictsea.com/api-terms.html).
Forecast guidance only — not for navigation. Every answer carries this
noticeand the credits its data requires. Show both to your users (API terms §4-5).
What it is, and what it is not
| For | Servers of third-party developers: a weather widget, a passage planner, a fleet dashboard. |
|---|---|
| Not for | The Predict Sea website and apps: they never call it and never send a position anywhere (ADR-29). Not for browsers or app binaries either: a key there is public (no CORS headers, ever). |
| Answers | Derived JSON: one position, one area, one question per call. |
| Does not serve | Raw grids or point tiles, coastlines, status files. Those stay on the static data plane: keyless, cacheable files anyone may read under DATA-CONTRACT.md. If you want whole fields, read the files. |
| Status | Free beta, no SLA. Keys self-serve (see Getting a key). Point data from the tiled global models only (see /v1/catalog, points: true); regional models (HRRR, NAM, ICON-EU, EWAM) and RTOFS appear in /v1/auto but have no point data in v1. |
/push/ on www.predictsea.com is the site's own notification door (first-party, no key); it is not part of this API.
Getting a key
Sign up at https://api.predictsea.com/signup with your email address, your name or organisation and a line on what you will build, and accept the API terms.
- We email you a link. It works once, for 30 minutes. Nothing after ten minutes? Look in your spam folder; we send at most two such emails a day to one address.
- The link opens a page with one button, Create my key. Opening the link does nothing by itself, so a mail scanner cannot use it up.
- The next page shows the key once, and a copy arrives by email. Store the key in your server's secret store or environment, then delete that email: a mailbox keeps a copy and can forward it.
A key looks like psk_ + 40 characters. We keep only its SHA-256 hash, so a lost key is
replaced, never recovered. A new key starts at 30 requests a minute and 1,000 weight units
a UTC day (below).
- One active key per address, for one person or organisation. Addresses are compared in a normalised form: lower case, a
+tagremoved, and for Gmail the dots too. Signing up again with an address that has a key sends a manage link instead of a second key. - Replace or delete at https://api.predictsea.com/manage. We email a link to the address the key was issued to. Replace my key shows and emails a new key, and the old one stops working at once: do this when a key is lost or may have leaked. Delete my account revokes the key and deletes your details. Each link allows one action.
- Disposable email services are not accepted, and the address's domain must be able to receive mail. Sign-up may be paused, or full for the day (then try again after 00:00 UTC); keys already issued keep working either way.
- The forms are plain HTML pages on api.predictsea.com: no script, no cookie, no tracking. The website only links to them.
- Higher limits, or a key issued by hand (60 requests a minute and 5,000 weight a day to start): email predictsea@outlook.com with your name or organisation, a contact address and what you are building.
Base URL and authentication
https://api.predictsea.com/v1
X-Api-Key: psk_…
The key goes in the X-Api-Key header only. A key in the query string (key=,
api_key=, apikey=, x-api-key=, any case) is refused with 400 key_in_query: URLs end
up in proxy logs and histories. Keep the key on your server.
Quotas and weights
| Limit | Default | Over it |
|---|---|---|
| Requests per minute (token bucket, refilled continuously) | 30 for a self-serve key, 60 for a hand-issued one | 429 rate_limited, Retry-After = seconds to the next token |
| Weight per UTC day | 1,000 for a self-serve key, 5,000 for a hand-issued one | 429 quota_exceeded, Retry-After = seconds to 00:00 UTC |
| Failed key attempts per client IP, an IPv6 client by its /64 (missing / invalid / revoked key, key in the URL) | 10, one back every 30 s | 429 too_many_failures for further failed attempts; a request with a valid key is never refused for them (a shared address, or a leaked key's old instances after a rotation, cannot lock you out) |
| Endpoint | Weight |
|---|---|
/catalog, /point, /auto, /storms, /obs | 1 |
/compare | 1 per model compared (5 by default), charged before any data is read |
Every answer reports the key's state in meta.quota. A 404 no_data counts (the work was
done); other errors do not. Limits can be raised per key: ask. Forecasts change with each
model run (every 6-12 h): cache on your side.
The envelope
{
"api": "predictsea/v1",
"gen": "20260927T063012-a1b2c3",
"init": "2026-09-27T00:00:00Z",
"age_h": 7.2,
"notice": "Forecast guidance only — not for navigation. Automated, unedited model output that may be late, incomplete or wrong; not an official forecast or warning. Always check the official marine forecasts and warnings for your area.",
"attribution": [
{"provider": "ECMWF", "models": ["ecmwf"],
"credit": "contains modified ECMWF data, © 2026 European Centre for Medium-Range Weather Forecasts (ECMWF), CC BY 4.0",
"license": "CC BY 4.0 (ECMWF open data)", "license_url": "https://creativecommons.org/licenses/by/4.0/",
"terms_url": "https://apps.ecmwf.int/datasets/licences/general/", "source_url": "https://www.ecmwf.int/",
"copyright": "© 2026 European Centre for Medium-Range Weather Forecasts (ECMWF)",
"notice": "This service is based on data and products of the European Centre for Medium-Range Weather Forecasts (ECMWF). …",
"disclaimer": "ECMWF does not accept any liability whatsoever for any error or omission in the data, …"}
],
"modification": "Predict Sea modifies every model's output: fields are cropped, regridded …",
"data": {},
"meta": {"request_id": "8f3c2a1b9d0e4f57", "generated_at": "2026-09-27T07:12:03Z", "weight": 1,
"quota": {"per_minute": 60, "day_limit": 5000, "day_used": 131, "day_remaining": 4869,
"resets_at": "2026-09-28T00:00:00Z"}}
}
| Field | Meaning |
|---|---|
gen | The forecast release served. One request reads one release, never a mix of two. |
init, age_h | Issue time and age (hours) of the oldest input the data uses: the model's run (point, auto), the oldest compared run (compare), the newest run of the catalogue (catalog), the feed's generated_at (storms, obs). |
notice | The not-for-navigation notice (storms add: tropical cyclone tracks are guidance, not an official warning). |
attribution | One entry per provider of the models the data names: point → the model; compare → the compared models (data.models); auto → model, why.alt and point_model; catalog → every model; storms → one entry per feed; obs → NOAA/NDBC. Nulls are kept. |
modification | How Predict Sea changes the model output (absent for obs). |
meta | Request id (also the X-Request-Id header: quote it when reporting a problem), time, weight, quota. |
Headers: Content-Type: application/json; charset=utf-8, Cache-Control: no-store,
X-Request-Id, X-Content-Type-Options: nosniff, Retry-After on 429 / 503. No CORS
headers, no cookies. GET and HEAD only.
Units and numbers
Wind and gusts in knots, directions in degrees true the wind or waves come from
(0-359), significant wave height in m, period in s, mean-sea-level pressure in
hPa, cloud in %, rain rate in mm/h, 2 m air temperature in °C,
visibility in km, distances in nautical miles, times ISO 8601 UTC to the second.
Values are rounded within half the files' quantisation step (kt 0.1, ° 1, m 0.01, s 0.1,
hPa 0.1, % 1, mm/h 0.1, °C 0.1, km 0.1), so they match what the website shows. A value
the model does not provide is null (ICON has no waves; a wave-only model no wind).
Parameters are strict: plain decimals (-?\d{1,3}(\.\d{1,6})?, no exponents or spaces),
lower-case ids, lat −90..90, lon −180..360 (folded to −180..180 and echoed folded),
times YYYY-MM-DDTHH:MM[:SS]Z. An unknown parameter is 400 unknown_param; a repeated
one is 400 invalid_param.
Endpoints
GET /v1/catalog
Models (run, cadence, freshness fresh / stale / old relative to the cadence, whether
they have point data), regions, basins and layers of the served release.
curl -s -H "X-Api-Key: $PSK" https://api.predictsea.com/v1/catalog
"data": {
"models": [{"id": "gfs", "name": "GFS", "provider": "NOAA/NCEP", "global": true, "domain": null,
"params": ["wind", "swell", "pres", "cloud", "sst", "precip", "temp", "vis"],
"extras": ["gust", "perpw", "swell1_h"], "points": true, "grid_deg": [0.25, 0.25],
"init": "2026-09-27T00:00:00Z", "cadence_h": 6, "age_h": 7.2, "freshness": "fresh",
"forecast_hours": [0, 3, 6, "…", 180]}],
"regions": [{"name": "nao", "title": "North Atlantic Ocean", "lat_min": 0, "lat_max": 60, "lon_min": -100, "lon_max": 0}],
"basins": [{"id": "north_atlantic", "name": "North Atlantic", "regions": ["nao", "lsg"]}],
"layers": ["wind", "gust", "swell", "swell1", "pres", "cloud", "precip", "temp", "vis", "sst", "current"]
}
GET /v1/point
Every hour of one model's run at a position — the numbers the website's point table shows (the same tile, the same sampling rule, DATA-CONTRACT §4.1).
| Parameter | |
|---|---|
lat, lon | required |
model | a model with points: true; default: the tiled wind model the website's automatic choice picks where the position opens (picked: "auto") |
max_lead | 0..384: rows with fhr ≤ it |
from | run (default, every hour) or now (hours valid at or after now − 3 h) |
curl -s -H "X-Api-Key: $PSK" "https://api.predictsea.com/v1/point?lat=48.5&lon=-5.0&from=now&max_lead=48"
"data": {
"lat": 48.5, "lon": -5.0, "model": "ecmwf", "picked": "auto",
"units": {"wind": "kt", "gust": "kt", "wind_dir": "deg_from_true", "wave": "m", "wave_dir": "deg_from_true",
"wave_period": "s", "mslp": "hPa", "cloud": "%", "rain": "mm/h", "temp": "degC", "vis": "km"},
"series": [{"valid": "2026-09-27T06:00:00Z", "fhr": 6, "wind_kt": 14.2, "gust_kt": 19.8, "wind_dir": 243,
"wave_m": 1.84, "wave_dir": 270, "wave_period_s": 9.1, "mslp_hpa": 1012.3,
"cloud_pct": 80, "rain_mmh": 0.2, "temp_c": 14.3, "vis_km": null}]
}
404 no_data: no point data at the position (a tile that is all land is not published) or
no hour has wind, pressure or waves there. Over land an atmospheric model still answers
(wind and pressure exist over land; waves are null).
GET /v1/compare
How far the models agree at a position: one metric across the fresh global models, on 6-hourly valid times of the default point model's run, with the spread per time, its agreement level and the website's verdict sentence (ADR-7, ADR-20).
| Parameter | |
|---|---|
lat, lon | required |
metric | wind (default, kt), gust (kt), dir (° from; the spread is circular, 0-180), wave (m), pres (hPa) |
models | default: up to 5 fresh models carrying the metric, GFS / ECMWF / ICON first; all: every fresh one; or 2-13 comma-separated ids with point data |
max_lead | valid times up to this many hours after the base run's start |
curl -s -H "X-Api-Key: $PSK" "https://api.predictsea.com/v1/compare?lat=48.5&lon=-5.0&metric=wind"
"data": {
"lat": 48.5, "lon": -5.0, "metric": "wind", "unit": "kt", "base": "ecmwf", "step_h": 6,
"valid": ["2026-09-27T06:00:00Z", "2026-09-27T12:00:00Z"],
"models": [{"id": "gfs", "name": "GFS", "init": "2026-09-27T00:00:00Z", "age_h": 7.2, "freshness": "fresh",
"values": [14.1, 17.9]}],
"spread": [3.2, 11.4], "agreement": ["hi", "lo"], "thresholds": {"hi": 5, "md": 10},
"verdict": {"kind": "early", "days": 0.3, "within": "10 kt",
"text": "Models already disagree on wind by more than 10 kt within the first day — low confidence; check again after the next runs."},
"excluded": [{"id": "aifs", "reason": "old", "age_h": 40.1}],
"deferred": ["gdps", "gefs"],
"regional_not_compared": ["ICON-EU"]
}
agreement per time: hi (spread below thresholds.hi), md (below thresholds.md),
lo, or null with fewer than two values. verdict.kind: single (fewer than two models
carry the metric here), agree (the whole range), early (they part within a day),
diverge (after days). excluded: runs too old to compare (older than four cycles);
deferred: fresh models the default cap left out (models=all compares them);
regional_not_compared: regional models covering the position, never compared.
GET /v1/auto
Which model the website shows for a layer in an area (ADR-26, ADR-34).
| Parameter | |
|---|---|
layer | required: one of layers in /v1/catalog |
region / basin / lat + lon | exactly one: a region name, a basin id, or a position (the smallest region holding it, else the world) |
valid | the valid time wanted, now − 24 h .. now + 16 days (default now) |
curl -s -H "X-Api-Key: $PSK" "https://api.predictsea.com/v1/auto?layer=wind®ion=northsea"
"data": {
"layer": "wind", "scope": {"type": "region", "id": "northsea", "title": "North Sea"},
"valid": "2026-09-27T12:00:00Z", "model": "iconeu",
"why": {"rule": "finest_regional", "km": 6.9, "alt": "ecmwf", "alt_km": 22.1},
"run": {"init": "2026-09-27T00:00:00Z", "ends": "2026-10-02T00:00:00Z", "freshness": "fresh"},
"point_model": "ecmwf"
}
why.rule: currents (layer current: RTOFS), world_freshest (the world: the freshest
global run), finest_regional (a regional model at least 20 % finer than the global pick
alt; km are effective grid spacings over the region), preferred_global. model may
have no point data; point_model is what /v1/point rides there.
GET /v1/storms
Active tropical cyclones: NHC advisories and ECMWF open-data tracks, canonical keys only
(kt, radius34_nm, classification; quadrant radii r34_nm / r50_nm / r64_nm as
[NE, SE, SW, NW] where NHC gives them), the shape of DATA-CONTRACT §7.
| Parameter | |
|---|---|
basin | the storm's basin (AL, EP, CP, WP, IO, SH …) |
members | 1 adds the ECMWF ensemble (ensemble, members: [lon, lat, tau_h] polylines, up to ~80 KB) |
curl -s -H "X-Api-Key: $PSK" "https://api.predictsea.com/v1/storms?basin=AL"
"data": {"generated_at": "2026-09-27T06:08:40Z", "count": 1,
"attribution": "Track guidance: NOAA/NHC and ECMWF open data — not an official warning; …",
"storms": [{"id": "al052026", "name": "Erin", "basin": "AL", "source": "NHC", "classification": "HU",
"category": 2, "lat": 25.1, "lon": -70.2, "intensity_kt": 90, "pressure_hpa": 968,
"track": ["…"], "forecast": ["…"], "cone": ["…"], "model_tracks": ["…"]}]}
Guidance, not an official warning. 503 feed_stale when the feed is older than 12 h
or absent: an empty list would read "no storms".
GET /v1/obs
The latest buoy and shore observations, US federal stations only: stations owned by NDBC, NOS (tide stations, NOAA-owned PORTS), a National Weather Service office, GLERL, the US Army Corps of Engineers, the FAA, Everglades National Park, Flower Garden Banks National Marine Sanctuary or another NOAA programme. Those are US Government works in the public domain; the rest of NDBC's feed (regional ocean-observing associations, universities, states, industry, foreign agencies) are their owners' data, which a keyed service does not redistribute. The website keeps showing them, credited by owner.
| Parameter | |
|---|---|
id / bbox / lat + lon | exactly one: a station id (case-insensitive); w,s,e,n (at most 60° × 60°, w > e crosses the dateline); a position |
radius_nm | 1..300 around lat + lon (default 60) |
limit | 1..200 (default 50) |
curl -s -H "X-Api-Key: $PSK" "https://api.predictsea.com/v1/obs?lat=42.35&lon=-70.65&radius_nm=30"
"data": {"generated_at": "2026-09-27T06:08:36Z", "feed_stale": false, "count": 3,
"stations": [{"id": "44013", "name": "BOSTON 16 NM East of Boston, MA", "owner": "NDBC", "type": "buoy",
"lat": 42.346, "lon": -70.651, "time": "2026-09-27T05:50:00Z", "age_min": 22,
"wdir": 230, "wspd_kt": 12.4, "gust_kt": 15.1, "wvht_m": 1.2, "dpd_s": 8.0,
"pres_hpa": 1012.2, "atmp_c": 12.1, "wtmp_c": 13.4, "distance_nm": 0.2}]}
Sorted by distance around a position (distance_nm), else by id. age_min is recomputed
from the report time; feed_stale is true when the feed is older than an hour. Winds are
as measured at the anemometer (4-5 m on buoys), not a 10 m wind. An unknown and a
non-federal id get the same 404 not_found.
GET /v1/openapi.yaml
The OpenAPI 3.1 spec. No key needed, not metered.
Errors
{"api": "predictsea/v1",
"error": {"status": 400, "code": "invalid_param", "param": "lat", "message": "lat must be a number from -90 to 90"},
"notice": "…", "meta": {"request_id": "…", "generated_at": "…", "quota": {"…": "…"}}}
| Status | code | When |
|---|---|---|
| 400 | missing_param, invalid_param, unknown_param | validation; param names the parameter |
| 400 | unknown_model, model_without_points, unknown_scope | an id the served catalogue does not have; a model without point data for point / compare; an unknown region or basin |
| 400 | key_in_query | a key in the URL |
| 401 | missing_key, invalid_key | |
| 403 | key_revoked | |
| 404 | not_found | an unknown path (answered only to a valid key); an unknown or non-federal station |
| 404 | no_data | point: no data at the position; auto: no model serves the layer there (metered) |
| 405 | method_not_allowed | not GET or HEAD |
| 429 | rate_limited, quota_exceeded, too_many_failures | see Quotas; honour Retry-After |
| 503 | no_release | no forecast release published (Retry-After: 60) |
| 503 | feed_stale | storms older than 12 h or absent; no observation feed (Retry-After: 600) |
| 503 | shutting_down | the service is restarting (Retry-After: 5) |
| 503 | overloaded | the service is busy: more requests at once than it takes (Retry-After: 2) |
| 500 | internal | anything else: quote meta.request_id |
meta.quota is filled once the key is known. On 429 and 503, wait Retry-After seconds;
do not retry in a tight loop and do not fan out to other endpoints.
Attribution: what your users must see
The data belongs to NOAA, ECMWF, DWD and ECCC under their licences (public domain, CC BY
4.0, the ECCC open-data licence), modified by Predict Sea. Wherever you show it, show the
attribution entries of the answer (API terms §4): each provider's credit and licence
link; for ECMWF also its notice, copyright and disclaimer and the words "contains
modified ECMWF data"; for ECCC the exact words "Data Source: Environment and Climate Change
Canada" and no ECCC or Government of Canada logos; NOAA's and ECCC's non-endorsement; the
modification statement; for storms, each feed's credit and "guidance, not an official
warning". On a small screen one tap from the data is enough, for example:
Wind 14 kt from WSW · ECMWF run 00Z · Forecast guidance only — not for navigation.
Data: contains modified ECMWF data, © 2026 ECMWF, CC BY 4.0 (licence) · processed by Predict Sea [details]
details → each attribution entry in full: credit, licence link, notice, copyright,
disclaimer, the modification statement
Versioning
/v1 changes additively: new fields, new parameters with defaults, new endpoints —
ignore what you do not know. A change that would break a v1 client is a new /v2, run
alongside /v1 for at least 90 days and announced by email to each key's contact.
The service's release is in openapi.yaml (info.version).
Privacy in brief
The coordinates and other parameters of a request are used to compute the answer and are never logged or stored by the API service. We keep, per key: your name or organisation, the contact email, what you said you would build, the terms version you accepted, the issue / revocation dates, whether the key came from sign-up or by hand, and a hash of the key; per key, UTC day and endpoint: a request count and the weight. Request logs name the endpoint, never the URL.
- An unconfirmed sign-up (the address, name and use) is held only in server memory, for up to 30 minutes, and never written to disk.
- Abuse counters, a keyed hash (HMAC) of your normalised address and your IP address (an IPv6 address by its /64), are counted in memory for at most 24 hours, never written or logged. IP addresses of failed key attempts are held in memory for minutes, never written.
- Mail goes through Amazon Web Services (Amazon SES), as our processor.
- Deletion: delete your account at
/manageat any time; the details of a key revoked 12 months ago are deleted.
The hosting provider's proxy logs are as the privacy page's "Server logs" describes. Details: privacy page (section "Developer API").
API reference · API terms · Privacy · Forecast status · Developer API