API reference
Generated from the OpenAPI spec, version 1.0.0-beta
Derived marine forecasts for third-party developers (keyed, free beta).
Point forecasts, model agreement, the automatic model choice, tropical-cyclone tracks and US federal buoy observations, computed from Predict Sea's published forecast releases by the same code the website runs.
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. Every answer carries
this notice and the attribution its data requires: show both to your users
(API terms: https://www.predictsea.com/api-terms.html).
Authentication: the X-Api-Key header only. A key in the query string is refused
(400 key_in_query). Get a key at https://api.predictsea.com/signup (free
beta; self-serve keys start at 30 requests a minute and 1,000 weight a day; more on
request).
Units: wind and gusts in knots, directions in degrees true the wind or waves come
FROM, wave height in metres, period in seconds, pressure in hPa (mean sea level),
cloud in %, rain in mm/h, temperature in °C, visibility in km, distances in nautical
miles, times ISO 8601 UTC. A value the model does not provide is null.
Versioning: v1 changes additively (new fields, new endpoints). A breaking change is
a new /v2, with at least 90 days of overlap and notice by email.
Basics
| Base URL | https://api.predictsea.com/v1 |
|---|---|
| Authentication | The X-Api-Key header. psk_ + 40 characters. Header only — never in a URL, a web page or an app binary. |
| Specification | https://api.predictsea.com/v1/openapi.yaml (OpenAPI 3.1.0, version 1.0.0-beta) |
| Contact | predictsea@outlook.com |
| Licence | Data under its upstream licences (see each answer's attribution); service under the API terms |
Endpoints
GET /v1/catalog
Models, regions, basins and layers of the served release
Weight 1. Lists each model's run, freshness (fresh / stale / old, relative to its publication cadence) and whether it has point data (points).
Authentication: the X-Api-Key header.
Responses
| Status | Meaning |
|---|---|
| 200 | The catalogue. |
| 400 | missing_param, invalid_param, unknown_param, unknown_model, model_without_points, unknown_scope, key_in_query. |
| 401 | missing_key, invalid_key. |
| 403 | key_revoked. |
| 429 | rate_limited (per-minute bucket), quota_exceeded (daily quota; resets 00:00 UTC), too_many_failures (failed key attempts from your address). Header: Retry-After. |
| 503 | no_release (Retry-After 60), feed_stale (storms / obs, Retry-After 600), shutting_down (Retry-After 5), overloaded (the service is busy, Retry-After 2). Header: Retry-After. |
Answer: data
Inside the envelope (CatalogData).
| Field | Type | Meaning |
|---|---|---|
data.models | array of object | |
data.models[].id | string | |
data.models[].name | string | |
data.models[].provider | string | |
data.models[].global | boolean | |
data.models[].domain | object | null | lat_min lat_max lon_min lon_max of a regional model. |
data.models[].params | array of string | |
data.models[].extras | array of string | |
data.models[].points | boolean | Has point data: usable with /point and /compare. |
data.models[].grid_deg | array of number | null | |
data.models[].init | string | null (date-time) | |
data.models[].cadence_h | integer | null | |
data.models[].age_h | number | null | |
data.models[].freshness | string | One of fresh, stale, old. |
data.models[].forecast_hours | array of integer | |
data.regions | array of object | |
data.regions[].name | string | |
data.regions[].title | string | |
data.regions[].lat_min | number | |
data.regions[].lat_max | number | |
data.regions[].lon_min | number | |
data.regions[].lon_max | number | |
data.basins | array of object | |
data.basins[].id | string | |
data.basins[].name | string | |
data.basins[].regions | array of string | |
data.layers | array of string |
GET /v1/point
Point forecast — every hour of one model at a position
One model's full run at a position, sampled from its point tile by the
normative rule of the data contract (the numbers the website's point table
shows). Default model: the tiled wind model the website's automatic choice
picks where the position opens (picked: auto). Weight 1.
Authentication: the X-Api-Key header.
Parameters
| Name | Required | Type | Default | Description |
|---|---|---|---|---|
lat | yes | string, pattern ^-?\d{1,3}(\.\d{1,6})?$ | Latitude, −90..90, plain decimal (at most 6 decimals, no exponent). | |
lon | yes | string, pattern ^-?\d{1,3}(\.\d{1,6})?$ | Longitude, −180..360 (folded to −180..180; the answer echoes the folded value). | |
model | no | string, pattern ^[a-z0-9_]{1,16}$ | A model id with points true in /catalog (default — Auto). | |
max_lead | no | integer, 0–384 | Only rows / times up to this many hours after the run's start (0..384). | |
from | no | string: run, now | run | run (default): every hour of the run; now: hours valid at or after now − 3 h. |
Responses
| Status | Meaning |
|---|---|
| 200 | The series. |
| 400 | missing_param, invalid_param, unknown_param, unknown_model, model_without_points, unknown_scope, key_in_query. |
| 401 | missing_key, invalid_key. |
| 403 | key_revoked. |
| 404 | no_data: no point data at this position (land, or no wind, pressure or waves); auto: no model serves the layer there. Metered. |
| 429 | rate_limited (per-minute bucket), quota_exceeded (daily quota; resets 00:00 UTC), too_many_failures (failed key attempts from your address). Header: Retry-After. |
| 503 | no_release (Retry-After 60), feed_stale (storms / obs, Retry-After 600), shutting_down (Retry-After 5), overloaded (the service is busy, Retry-After 2). Header: Retry-After. |
Answer: data
Inside the envelope (PointData).
| Field | Type | Meaning |
|---|---|---|
data.lat | number | |
data.lon | number | Folded to −180..180. |
data.model | string | |
data.picked | string | One of auto, request. |
data.units | object of string values | |
data.series | array of object | |
data.series[].valid | string (date-time) | |
data.series[].fhr | integer | Hours after the run's start. |
data.series[].wind_kt | number | null | 10 m wind, kt, 0.1. |
data.series[].gust_kt | number | null | |
data.series[].wind_dir | integer | null | ° true the wind comes from, 0..359. |
data.series[].wave_m | number | null | Significant wave height, m, 0.01. |
data.series[].wave_dir | integer | null | ° true the waves come from. |
data.series[].wave_period_s | number | null | |
data.series[].mslp_hpa | number | null | |
data.series[].cloud_pct | integer | null | |
data.series[].rain_mmh | number | null | |
data.series[].temp_c | number | null | 2 m air temperature. |
data.series[].vis_km | number | null |
GET /v1/compare
Model agreement at a position
One metric across the fresh global models, on 6-hourly valid times, with the
spread per time, its agreement level (hi below thresholds.hi, md below
thresholds.md, else lo) and the website's verdict. Weight = the number of
models compared.
Authentication: the X-Api-Key header.
Parameters
| Name | Required | Type | Default | Description |
|---|---|---|---|---|
lat | yes | string, pattern ^-?\d{1,3}(\.\d{1,6})?$ | Latitude, −90..90, plain decimal (at most 6 decimals, no exponent). | |
lon | yes | string, pattern ^-?\d{1,3}(\.\d{1,6})?$ | Longitude, −180..360 (folded to −180..180; the answer echoes the folded value). | |
metric | no | string: wind, gust, dir, wave, pres | wind | wind / gust (kt), dir (° from, circular spread), wave (m), pres (hPa). |
models | no | string | all (every fresh model carrying the metric), or 2–13 comma-separated model ids with point data. Default: up to 5, GFS / ECMWF / ICON first while fresh. | |
max_lead | no | integer, 0–384 | Only rows / times up to this many hours after the run's start (0..384). |
Responses
| Status | Meaning |
|---|---|
| 200 | The comparison. |
| 400 | missing_param, invalid_param, unknown_param, unknown_model, model_without_points, unknown_scope, key_in_query. |
| 401 | missing_key, invalid_key. |
| 403 | key_revoked. |
| 429 | rate_limited (per-minute bucket), quota_exceeded (daily quota; resets 00:00 UTC), too_many_failures (failed key attempts from your address). Header: Retry-After. |
| 503 | no_release (Retry-After 60), feed_stale (storms / obs, Retry-After 600), shutting_down (Retry-After 5), overloaded (the service is busy, Retry-After 2). Header: Retry-After. |
Answer: data
Inside the envelope (CompareData).
| Field | Type | Meaning |
|---|---|---|
data.lat | number | |
data.lon | number | |
data.metric | string | |
data.unit | string | |
data.base | string | null | The model whose run sets the valid times (/point's default here). |
data.step_h | const 6 | |
data.valid | array of string (date-time) | |
data.models | array of object | |
data.models[].id | string | |
data.models[].name | string | |
data.models[].init | string | null (date-time) | |
data.models[].age_h | number | null | |
data.models[].freshness | string | |
data.models[].values | array of number | null | |
data.spread | array of number | null | |
data.agreement | array of string | null | |
data.thresholds | object | |
data.thresholds.hi | number | |
data.thresholds.md | number | |
data.verdict | object | |
data.verdict.kind | string | One of single, agree, early, diverge. |
data.verdict.days | number | null | |
data.verdict.within | string | |
data.verdict.text | string | |
data.excluded | array of object | |
data.excluded[].id | string | |
data.excluded[].reason | const "old" | |
data.excluded[].age_h | number | null | |
data.deferred | array of string | Fresh models the default cap left out (models=all compares them). |
data.regional_not_compared | array of string | Regional models covering the position (never compared). |
GET /v1/auto
The model the website picks for a layer in an area
Give exactly one of region, basin, or lat + lon (the smallest region
holding the position, else the world). point_model is the tiled model a point
forecast rides there (/point's default). model may have no point data (HRRR,
NAM, ICON-EU, EWAM, RTOFS). Weight 1.
Authentication: the X-Api-Key header.
Parameters
| Name | Required | Type | Default | Description |
|---|---|---|---|---|
layer | yes | string: wind, gust, swell, swell1, pres, cloud, precip, temp, vis, sst, current | ||
region | no | string, pattern ^[a-z0-9_]{1,16}$ | A region name from /catalog. | |
basin | no | string, pattern ^[a-z0-9_]{1,16}$ | A basin id from /catalog. | |
lat | no | string, pattern ^-?\d{1,3}(\.\d{1,6})?$ | Latitude −90..90 (with lon). | |
lon | no | string, pattern ^-?\d{1,3}(\.\d{1,6})?$ | Longitude −180..360 (with lat). | |
valid | no | string, pattern ^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?Z$ | The valid time wanted (UTC), within now − 24 h .. now + 16 days. Default now. |
Responses
| Status | Meaning |
|---|---|
| 200 | The pick. |
| 400 | missing_param, invalid_param, unknown_param, unknown_model, model_without_points, unknown_scope, key_in_query. |
| 401 | missing_key, invalid_key. |
| 403 | key_revoked. |
| 404 | no_data: no point data at this position (land, or no wind, pressure or waves); auto: no model serves the layer there. Metered. |
| 429 | rate_limited (per-minute bucket), quota_exceeded (daily quota; resets 00:00 UTC), too_many_failures (failed key attempts from your address). Header: Retry-After. |
| 503 | no_release (Retry-After 60), feed_stale (storms / obs, Retry-After 600), shutting_down (Retry-After 5), overloaded (the service is busy, Retry-After 2). Header: Retry-After. |
Answer: data
Inside the envelope (AutoData).
| Field | Type | Meaning |
|---|---|---|
data.layer | string | |
data.scope | object | |
data.scope.type | string | One of world, basin, region. |
data.scope.id | string | null | |
data.scope.title | string | |
data.valid | string (date-time) | |
data.model | string | |
data.why | object | |
data.why.rule | string | One of currents, world_freshest, finest_regional, preferred_global. |
data.why.km | number | null | Effective grid spacing over the region, km. |
data.why.alt | string | null | The global model a regional pick beat. |
data.why.alt_km | number | null | |
data.run | object | |
data.run.init | string | null (date-time) | |
data.run.ends | string | null (date-time) | |
data.run.freshness | string | |
data.point_model | string | null |
GET /v1/storms
Active tropical cyclones — guidance, not an official warning
NHC advisories and ECMWF open-data tracks with canonical keys (kt,
radius34_nm, classification; quadrant radii r34_nm / r50_nm /
r64_nm [NE, SE, SW, NW] where given). 503 feed_stale when the feed is
older than 12 h (an empty list would read "no storms"). Weight 1.
Authentication: the X-Api-Key header.
Parameters
| Name | Required | Type | Default | Description |
|---|---|---|---|---|
basin | no | string, pattern ^[A-Z]{2}$ | The storm's basin (AL, EP, CP, WP, IO, SH …). | |
members | no | string: 0, 1 | 0 | 1: include the ECMWF ensemble (ensemble, members: [lon, lat, tau_h] polylines). |
Responses
| Status | Meaning |
|---|---|
| 200 | The storms. |
| 400 | missing_param, invalid_param, unknown_param, unknown_model, model_without_points, unknown_scope, key_in_query. |
| 401 | missing_key, invalid_key. |
| 403 | key_revoked. |
| 429 | rate_limited (per-minute bucket), quota_exceeded (daily quota; resets 00:00 UTC), too_many_failures (failed key attempts from your address). Header: Retry-After. |
| 503 | no_release (Retry-After 60), feed_stale (storms / obs, Retry-After 600), shutting_down (Retry-After 5), overloaded (the service is busy, Retry-After 2). Header: Retry-After. |
Answer: data
Inside the envelope (StormsData).
| Field | Type | Meaning |
|---|---|---|
data.generated_at | string (date-time) | |
data.count | integer | |
data.attribution | string | null | The feed's own credit line. |
data.storms | array of object | |
data.storms[].id | string | |
data.storms[].name | string | |
data.storms[].basin | string | |
data.storms[].source | string | One of NHC, ECMWF. |
data.storms[].classification | string | |
data.storms[].category | integer | |
data.storms[].lat | number | |
data.storms[].lon | number | |
data.storms[].intensity_kt | number | null | |
data.storms[].pressure_hpa | number | null | |
data.storms[].track | array of object | |
data.storms[].forecast | array of object | |
data.storms[].cone | array of array of number | |
data.storms[].model_tracks | array of object |
GET /v1/obs
Latest buoy and shore observations — US federal stations only
Give exactly one of id, bbox, or lat + lon (with radius_nm). Only
stations a US federal agency owns (NDBC, NOS, NWS, GLERL, USACE, FAA, NPS,
NOAA programs) are served. Sorted by distance around a position, else by id.
An unknown and a non-federal id both answer 404 not_found. Weight 1.
Authentication: the X-Api-Key header.
Parameters
| Name | Required | Type | Default | Description |
|---|---|---|---|---|
id | no | string, pattern ^[A-Za-z0-9]{3,8}$ | A station id (case-insensitive). | |
bbox | no | string | w,s,e,n in degrees, at most 60° × 60°; w > e crosses the dateline. | |
lat | no | string, pattern ^-?\d{1,3}(\.\d{1,6})?$ | ||
lon | no | string, pattern ^-?\d{1,3}(\.\d{1,6})?$ | ||
radius_nm | no | integer, 1–300 | 60 | Nautical miles around lat + lon. |
limit | no | integer, 1–200 | 50 |
Responses
| Status | Meaning |
|---|---|
| 200 | The stations. |
| 400 | missing_param, invalid_param, unknown_param, unknown_model, model_without_points, unknown_scope, key_in_query. |
| 401 | missing_key, invalid_key. |
| 403 | key_revoked. |
| 404 | not_found: an unknown or a non-federal station id. |
| 429 | rate_limited (per-minute bucket), quota_exceeded (daily quota; resets 00:00 UTC), too_many_failures (failed key attempts from your address). Header: Retry-After. |
| 503 | no_release (Retry-After 60), feed_stale (storms / obs, Retry-After 600), shutting_down (Retry-After 5), overloaded (the service is busy, Retry-After 2). Header: Retry-After. |
Answer: data
Inside the envelope (ObsData).
| Field | Type | Meaning |
|---|---|---|
data.generated_at | string | null (date-time) | |
data.feed_stale | boolean | The feed is older than 1 h. |
data.count | integer | |
data.stations | array of object | |
data.stations[].id | string | |
data.stations[].name | string | |
data.stations[].owner | string | |
data.stations[].type | string | |
data.stations[].lat | number | |
data.stations[].lon | number | |
data.stations[].time | string (date-time) | |
data.stations[].age_min | integer | null | |
data.stations[].wdir | number | null | |
data.stations[].wspd_kt | number | null | As measured at the anemometer (4–5 m on buoys), not a 10 m wind. |
data.stations[].gust_kt | number | null | |
data.stations[].wvht_m | number | null | |
data.stations[].dpd_s | number | null | |
data.stations[].pres_hpa | number | null | |
data.stations[].atmp_c | number | null | |
data.stations[].wtmp_c | number | null | |
data.stations[].distance_nm | number |
GET /v1/openapi.yaml
This document (no key needed, not metered)
Authentication: none (no key needed).
Responses
| Status | Meaning |
|---|---|
| 200 | OpenAPI 3.1. Type application/yaml. |
The envelope
Every answer is this object; the endpoint's own answer is in data. Show notice and the attribution entries with the data (API terms §4-5).
| Field | Type | Meaning |
|---|---|---|
api | const "predictsea/v1" | |
gen | string | null | The forecast release served (YYYYMMDDTHHMMSS-xxxxxx). |
init | string | null (date-time) | Issue time of the oldest input the data uses (a model's run, a feed's generated_at). |
age_h | number | null | Hours since init, 1 decimal. |
notice | string | Not-for-navigation notice: show it with the data. |
attribution | array of Attribution | |
attribution[].provider | string | null | For example ECMWF, NOAA/NCEP, DWD, ECCC/MSC, NOAA/NHC, NOAA/NDBC. |
attribution[].models | array of string | |
attribution[].credit | string | |
attribution[].license | string | null | |
attribution[].license_url | string | null | |
attribution[].terms_url | string | null | |
attribution[].source_url | string | null | |
attribution[].copyright | string | null | |
attribution[].notice | string | null | |
attribution[].disclaimer | string | null | |
attribution[].note | string | |
modification | string | null | How Predict Sea modifies the model output (absent for obs). |
data | object | |
meta | Meta | |
meta.request_id | string | |
meta.generated_at | string (date-time) | |
meta.weight | integer | |
meta.quota | Quota | null | |
meta.quota.per_minute | integer | |
meta.quota.day_limit | integer | Weight units per UTC day. |
meta.quota.day_used | integer | |
meta.quota.day_remaining | integer | |
meta.quota.resets_at | string (date-time) |
Errors
An error answer carries error in place of data, with the notice and meta:
| Field | Type | Meaning |
|---|---|---|
error | object | |
error.status | integer | |
error.code | string | One of the codes below. |
error.param | string | |
error.message | string |
| Code | Status |
|---|---|
missing_param | 400 |
invalid_param | 400 |
unknown_param | 400 |
unknown_model | 400 |
model_without_points | 400 |
unknown_scope | 400 |
key_in_query | 400 |
missing_key | 401 |
invalid_key | 401 |
key_revoked | 403 |
not_found | 404 |
no_data | 404 |
method_not_allowed | — |
rate_limited | 429 |
quota_exceeded | 429 |
too_many_failures | 429 |
no_release | 503 |
feed_stale | 503 |
shutting_down | 503 |
overloaded | 503 |
internal | — |
A status of “—”: no operation lists that code (it can answer any request); the guide's error table gives every code with its status. On 429 and 503, wait Retry-After seconds.
The raw specification: https://api.predictsea.com/v1/openapi.yaml.
API guide · API terms · Privacy · Forecast status · Developer API