Coming soon - Mareel Earth is in private development. The API is live, but accounts are not open yet. Early access: [email protected]

← All docs

Waters API

The marine water-state forecast: waves (including resolved swell partitions), wind, surface currents, sea-surface temperature and depth for any ocean coordinate, out to 10 days. Served from Mareel's own pre-baked global grid - a region-aware blend of the best open models, finest near the coast (~2-3 km) and coarser offshore, with coastal wave physics (shoaling, refraction, depth-limited breaking) baked in nearshore. Waters is deliberately tide-free: tides are the separate Tides product; combine the two client-side.

Endpoint

GET /v1/waters

ParamTypeDefaultNotes
lat, lonnumberrequired-90..90 / -180..180
varscomma listallsubset of the variable keys below

The response resolves your coordinate to the nearest grid cell (resolved.nearest_cell_km tells you the snap distance). Points more than 60 km from any ocean cell return 422 no_ocean_cell.

Response

{
  "query":    { "lat": 58.0, "lon": 11.0 },
  "resolved": { "nearest_cell_km": 1.2, "cell": [58.01, 11.02],
                "grid_cycle": "2026-07-12T06:00:00+00:00", "steps": 93 },
  "variables": ["wave_height", "wave_period_peak", "wind_speed", ...],
  "steps": [
    { "time": "2026-07-12T06:00:00+00:00",
      "values": { "wave_height": 1.4, "wave_direction": 245.0, "swell1_height": 1.1,
                  "wind_speed": 12.3, "sst": 16.8, "current_speed": 0.3,
                  "current_hazard": "calm", "depth_m": 42.5, ... } },
    ...
  ],
  "meta": { "model": "pre-baked region-aware grid", "cache": "static-grid",
            "took_ms": 1.8, "disclaimers": ["Not for navigation or safety-of-life decisions; ..."] }
}

A variable a source cannot reach at a given step is null - the series ends honestly where the underlying model runs out rather than padding stale values.

Variables

KeyUnitConventionPrimary sources
wave_heightmsignificant heightNOAA GFS-Wave · Copernicus MFWAM · regional basin models
wave_period_peak / wave_period_meanssame as waves
wave_directiondegcoming fromsame as waves
swell1_height / swell1_period / swell1_directionm / s / degprimary swell partition, direction fromsame as waves
swell2_height / swell2_period / swell2_directionm / s / degsecondary swell partitionsame as waves
windwave_height / windwave_period / windwave_directionm / s / deglocal wind seasame as waves
wind_speed / wind_gustknots10 mECMWF IFS · ICON-D2/EU · HRRR · HRDPS regionally
wind_directiondegcoming fromsame as wind
current_speedm/ssurfaceNOAA RTOFS · US/EU HF-radar nearshore
current_directiondeggoing tosame as currents
current_hazardlabelcalm / strong / danger (derived from current_speed)derived
sst°Csea surfaceNOAA RTOFS anchored to MUR / OSTIA / OISST analyses
depth_mmtide-free depthGEBCO 2024 (15″)

Where several models cover a point, the finest genuinely-available source wins (e.g. a 2 km regional model beats the global one inside its footprint) - deterministic per cell, no averaging across resolutions.

Field invariants

What a client may rely on, field by field. A 200 body has exactly five keys - query, resolved, variables, steps, meta - passed through verbatim from the grid; errors always use { "error": { "code", "message" } }. Responses that reach the handler (200/400/422 and handler 5xx) carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (the per-key, per-product bucket), as does 429 - the early auth gates (401/402/403) do not - plus X-Quota-Limit and X-Quota-Used when your plan has a quota; bodies over 1 KB are gzipped when you accept it, and keyed 200s carry no cache header. The keyless /v1/demo/waters mirror adds a _proxy_ms diagnostic - the keyed route never does.

FieldYou may rely on
query.lat / query.lonNumbers - your coordinates echoed back exactly as parsed, not rounded, not snapped. Per point. Never null on a 200.
resolved.grid_cycleString, ISO 8601 UTC with a +00:00 suffix. The first timestep of the loaded bake, so it always equals steps[0].time. Grid-global: one value per loaded grid, never per cell or per region, and a single response never mixes two cycles. Monotone non-decreasing per serving process - see the rollout caveat below.
resolved.published_at / bake_idpublished_at is the ISO 8601 UTC time this grid was PUBLISHED; bake_id is the same instant as a compact monotonic id (YYYYMMDDTHHMM). Unlike grid_cycle (the forecast BASE time, which only advances on a NEW-base bake), these change on EVERY publish - including a same-base re-bake with fresher inputs. Key your refresh cadence on published_at to catch every publication, not every other one. baked_at is an exact alias of published_at (same value). Grid-global; null only if the publish name isn't the expected stamp.
resolved.nearest_cell_kmNumber - great-circle kilometres from your point to the served cell center, rounded to 2 decimals. Per point. On any 200 it is 0..60: beyond 60 km the request is 422 no_ocean_cell instead, so a 200 proves the snap distance is within 60 km. Never null on a 200.
resolved.cellTwo-element array [cell_lat, cell_lon] - the center of the ocean cell actually served, floats rounded to 4 decimals. Per point. This pair plus nearest_cell_km is the full snap disclosure. Never null on a 200.
resolved.stepsInteger, at least 1 - the timestep count of the loaded bake; always equals steps.length. Grid-global per cycle: identical for every point on the same grid_cycle, changing only when a new bake is published. Never null.
variablesArray of strings - the variable keys actually served. With ?vars= it preserves your request order filtered to keys that exist in the bake; unknown names are silently dropped (all-unknown yields variables: [] and empty values, still a 200, not an error). Without ?vars=, the full baked list in bake order. Grid-global per cycle.
stepsArray of { time, values } objects, length equal to resolved.steps, strictly ascending in time. The axis is tapered, not uniform (hourly to +48 h, 3-hourly to +120 h, 6-hourly to +240 h) - read each step's time, never assume fixed spacing.
steps[].timeString, same ISO 8601 UTC +00:00 format as grid_cycle. steps[0].time equals resolved.grid_cycle. Strictly increasing.
steps[].values.<var>Number rounded to 2 decimals, or null. null means the baked value was not finite - no data for that variable at that cell and step. Values are never zero-filled to hide gaps, so 0 is always a real physical zero. Per point, per step, per variable.
steps[].values.current_hazardDerived string, exactly one of calm / strong / danger, computed from current_speed in m/s (>1.0 danger, >0.5 strong, else calm). Additive and optional: present only when current_speed is among the served variables and finite at that step, absent otherwise - never null. A data label, not a rating, and it does not appear in variables.
meta.modelConstant string "pre-baked region-aware grid". Global, never varies per point.
meta.cacheConstant string "static-grid". Global.
meta.took_msNumber - milliseconds of grid lookup time only, rounded to 2 decimals (excludes the web tier, retries and network). Per-request diagnostic; no bound is guaranteed and it is not contract data.
meta.disclaimersArray of strings, currently exactly one constant element (the not-for-navigation disclaimer). Global; may gain entries but never becomes null.

The one honest wrinkle in resolved.grid_cycle: when a new bake is published, the serving workers hot-reload staggered (roughly 30 s apart) so the grid never stops serving. During that window two consecutive requests can land on different workers and see the new cycle then the old one - grid_cycle may regress by exactly one cycle for under a minute, then is monotone again. Key your caches by the cycle value, not by "latest ever seen".

How and when the data updates

The grid is re-baked whenever an upstream model publishes a new cycle, at most every ~2-3 hours, and swapped in atomically - you never read a half-written forecast. Read the data's age from resolved.grid_cycle (the forecast base time) and each step's time.

SourceFeedsTypical cycles
NOAA GFS-Wave 0.25°waves global4× / day
Copernicus MFWAM 1/12° + Baltic/Black Sea/IBI/Med basin modelswaves regional1-2× / day
ECMWF IFS 0.25°wind global2× / day
DWD ICON-D2 2.2 km / ICON-EU 6.5 km, NOAA HRRR 3 km, ECCC HRDPS 2.5 kmwind regionalhourly - 6-hourly
NOAA RTOFS 1/12°currents + SSTdaily
JPL MUR / UKMO OSTIA / NOAA OISSTSST anchoring analysesdaily
US IOOS + EU HF-radarcoastal currentshourly

Horizon & time steps: 10 days (240 h) with a tapered axis - hourly to +48 h, 3-hourly to +120 h, 6-hourly to +240 h (~93 steps). Short-range regional detail hands over to the global backbone where a fine model's horizon ends.

Errors & retries

Every error uses the shared envelope from the docs index - { "error": { "code", "message" } }. The full catalog for /v1/waters, with the retry signal where one exists:

HTTPCodeMeaningRetry-After
400bad_requestlat or lon missing from the query. A present but non-numeric lat/lon is caught deeper in the stack and surfaces as 503 waters_error, not 400.-
401unauthorizedMissing or invalid API key (Authorization: Bearer <key> or ?key=). Checked before everything else.-
402quota_exceededMonthly points quota exceeded. Carries X-Quota-Limit / X-Quota-Used so you can see where you stand.-
403product_not_enabledValid key whose account does not include Waters.-
422no_ocean_cellThe nearest ocean grid cell is more than 60 km from your point - genuinely no Waters coverage (deep inland). Returned immediately and never retried on our side; it is a fact about the coordinate, not a fault.-
429rate_limitedPer-key per-minute rate limit exhausted. Carries X-RateLimit-Limit.seconds until the next call is allowed
500internal_errorUnexpected error on our side.-
503coming_soonThe product is switched off. Dormant today - Waters is live - but if it were ever flipped back it fires for every key, regardless of entitlement.-
503not_yet_availableServing misconfiguration on our side - the grid worker address is missing. Not something a client can fix.-
503waters_errorTransient grid failure after our own two attempts with backoff - a worker mid-reload or a transport stall. These clear in seconds to tens of seconds.30

The one worth automating: on 503 waters_error, honor Retry-After: 30 and retry once - the serving workers reload staggered, so the retry usually lands on a worker that is serving. Do not retry 422 no_ocean_cell: the answer will not change.

Try it without a key

/v1/demo/waters?lat=..&lon=.. is the keyless showcase window (same grid, CDN-cached, no SLA) - fine for evaluation, not for production. Production traffic belongs on /v1/waters with your key.

Observations

GET /v1/observations

Live in-situ measurements (wave buoys, HF-radar) near a coordinate - the ground truth alongside the forecast. Same Waters entitlement.