← Developer API

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 notice and the credits its data requires. Show both to your users (API terms §4-5).

What it is, and what it is not

ForServers of third-party developers: a weather widget, a passage planner, a fleet dashboard.
Not forThe 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).
AnswersDerived JSON: one position, one area, one question per call.
Does not serveRaw 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.
StatusFree 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.

  1. 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.
  2. 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.
  3. 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).

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

LimitDefaultOver it
Requests per minute (token bucket, refilled continuously)30 for a self-serve key, 60 for a hand-issued one429 rate_limited, Retry-After = seconds to the next token
Weight per UTC day1,000 for a self-serve key, 5,000 for a hand-issued one429 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 s429 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)
EndpointWeight
/catalog, /point, /auto, /storms, /obs1
/compare1 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"}}
}
FieldMeaning
genThe forecast release served. One request reads one release, never a mix of two.
init, age_hIssue 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).
noticeThe not-for-navigation notice (storms add: tropical cyclone tracks are guidance, not an official warning).
attributionOne 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.
modificationHow Predict Sea changes the model output (absent for obs).
metaRequest 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, lonrequired
modela model with points: true; default: the tiled wind model the website's automatic choice picks where the position opens (picked: "auto")
max_lead0..384: rows with fhr ≤ it
fromrun (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, lonrequired
metricwind (default, kt), gust (kt), dir (° from; the spread is circular, 0-180), wave (m), pres (hPa)
modelsdefault: 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_leadvalid 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
layerrequired: one of layers in /v1/catalog
region / basin / lat + lonexactly one: a region name, a basin id, or a position (the smallest region holding it, else the world)
validthe 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&region=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
basinthe storm's basin (AL, EP, CP, WP, IO, SH …)
members1 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 + lonexactly one: a station id (case-insensitive); w,s,e,n (at most 60° × 60°, w > e crosses the dateline); a position
radius_nm1..300 around lat + lon (default 60)
limit1..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": {"…": "…"}}}
StatuscodeWhen
400missing_param, invalid_param, unknown_paramvalidation; param names the parameter
400unknown_model, model_without_points, unknown_scopean id the served catalogue does not have; a model without point data for point / compare; an unknown region or basin
400key_in_querya key in the URL
401missing_key, invalid_key
403key_revoked
404not_foundan unknown path (answered only to a valid key); an unknown or non-federal station
404no_datapoint: no data at the position; auto: no model serves the layer there (metered)
405method_not_allowednot GET or HEAD
429rate_limited, quota_exceeded, too_many_failuressee Quotas; honour Retry-After
503no_releaseno forecast release published (Retry-After: 60)
503feed_stalestorms older than 12 h or absent; no observation feed (Retry-After: 600)
503shutting_downthe service is restarting (Retry-After: 5)
503overloadedthe service is busy: more requests at once than it takes (Retry-After: 2)
500internalanything 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.

The hosting provider's proxy logs are as the privacy page's "Server logs" describes. Details: privacy page (section "Developer API").