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

← All docs

Tides API

Tide heights for any ocean coordinate, synthesized on demand from a global grid of harmonic constants (FES2022-based, 38M+ points). There is no forecast staleness: astronomical tides are computed live from the constants for exactly the times you ask for. Every response tells you which grid node served your point and how far away it is - no hidden interpolation.

Endpoints

Heights

GET /v1/tides/heights

ParamTypeDefaultNotes
lat, lonnumberrequired-90..90 / -180..180
start, endISO 8601 + offsetnow → now+dayse.g. 2026-07-12T00:00:00+00:00
intervalint minutes30sample spacing, 5..120
daysint7horizon when start/end omitted, 1..30
datumstringMSLvertical datum code - see Datums below
extremesbooleanfalsetrue also folds the high/low events into this response - no second call to /extremes
{
  "query": { "lat": 58.0, "lon": 11.0 },
  "resolved": { "lat": 58.0, "lon": 11.0, "snapped_from_land": false, "snap_distance_km": 0 },
  "grid": { "lat": 58.02, "lon": 11.03, "distance_km": 2.4 },
  "interval_min": 30,
  "heights": [ { "time": "2026-07-12T10:00:00+00:00", "height_m": 0.12 }, ... ],
  "meta": { "model": "FES2022", "datum": "MSL", "datum_official": false, "cache": "...", "attribution": "..." }
}

Extremes (high / low water)

GET /v1/tides/extremes

Same parameters as heights (no interval). Returns tide events instead of a series: extremes: [{ time, type: "HIGH"|"LOW", height_m }].

Current level + next extreme

GET /v1/tides/current

lat, lon (and optional datum). Returns now: { time, height_m } (nearest sample to the current minute) and next: { time, type, height_m } - the next high or low water.

Tidal datums

GET /v1/tides/datums

The full datum family for a coordinate - LAT, HAT, MHWS, MHW, MLLW, MLWS and more - each referenced to model mean sea level and tagged official: true|false (computed from 18.6 years of synthesis, or the official national value where available). Also returns the local chart_datum, tidal character and form factor.

Nearest grid node

GET /v1/stations

Transparency endpoint: the nearest harmonic grid node to your coordinate, its distance, and the model's native resolution - so you always know exactly what serves your point.

Datums

Heights default to MSL (model mean sea level). Pass ?datum=LAT (or MLLW, MHWS, HAT, …) to re-reference the whole series - the response's meta.datum and meta.datum_official confirm what was applied. Unknown or unavailable datums for a location return an explanatory error rather than silently serving MSL.

Freshness & accuracy

Tides are deterministic astronomy: values are computed at request time from harmonic constants, so there is nothing to go stale. The model validates at centimetre-level against the FES reference (mean synthesis error ~0.6 cm). Points slightly inland snap to the nearest ocean cell (resolved.snapped_from_land / snap_distance_km); genuinely inland points return 422 no_ocean_tide.

Field invariants

Everything in this section is contract, not description - a client may rely on it. Request defaults are deterministic: an omitted start snaps to the current whole UTC hour, so two clients asking in the same hour get identical timestamps, while an explicit start is honored exactly and anchors the sample lattice. An omitted end is start + days × 86,400 s (days accepts 1..30 - default 7 on heights and extremes, 2 on current); end at or before start, or a span over 366 days, is a 400. interval accepts 5..120 minutes, default 30 - out-of-range values are rejected with 400, never silently clamped. datum is uppercased server-side and validated per location against the resolved datum family.

FieldWhat you may rely on
query.{lat,lon}The coordinates you sent, echoed back exactly as parsed (numerically exact JSON numbers) - never snapped, never rounded.
resolved.{lat,lon}The water point tides were computed for. In every 200 it is identically a grid node: resolved.lat == grid.lat and resolved.lon == grid.lon, on every path.
resolved.snapped_from_landBoolean. false exactly when the nearest ocean grid node is within 6 km of your query; when false, method is always nearest_water and snap_distance_km <= 6.
resolved.snap_distance_kmKilometres, rounded to 3 decimals. On a successful perpendicular snap it is the distance from your query to the actual shoreline - the meaningful number; otherwise the distance to the nearest ocean grid node.
resolved.methodEnum: nearest_water | perpendicular. perpendicular implies snapped_from_land: true (we stepped out to the real OSM shoreline). The converse does not hold: snapped_from_land: true with nearest_water is the degraded fallback when the coastline worker is unavailable - the API stays up, snap quality is reduced.
resolved.coastOptional {lat, lon} of the shoreline point stepped from, 3 decimals. Present only when method is perpendicular; absent (not null) otherwise. The datums endpoint never emits method or coast.
grid.{lat,lon}The nearest ocean FES grid node actually used for synthesis. The node search only sees ocean nodes, so enclosed non-ocean seas like the Caspian can never be selected.
grid.distance_km3 decimals. Measured from your query on the open-water path, and from the stepped-out sea point on the perpendicular path. At most 6 km on both coastline-successful paths; it can exceed 6 only in the degraded fallback.
interval_minHeights only: echo of the effective sampling interval in minutes after defaults and clamping (5..120, default 30). Not present on extremes, current or datums.
heights[].timeLattice contract: every timestamp is exactly start + n × interval, the first sample sits at start, and every timestamp lies within [start, end]. Seconds-resolution ISO 8601 UTC with an explicit +00:00. The lattice holds whether the series came from cache or fresh synthesis.
heights[].height_mMetres relative to meta.datum, rounded to 3 decimals. Default is model MSL (zero-mean); with ?datum= each value is the MSL height minus that datum's offset, so a LAT-referenced series reads positive.
extremes[].{time,type,height_m}Event times, not on the lattice: each extreme is refined sub-sample (a parabola through the extremum and its neighbours, rounded to whole seconds), so times land between samples. type is strictly HIGH or LOW - flats are excluded. Window-filtered inclusive to [start, end]; height_m datum-shifts identically to heights.
current: nowThe height sample nearest server time - within 15 minutes of true now at the default 30-minute interval. Shape {time, height_m}; null only in the degenerate empty-series case.
current: nextThe first extreme strictly after server now, else null. The default window is ~2 days so a next extreme is in range in any tidal sea - but explicit start/end are honored, so a short or past window can legitimately return null.
meta.modelThe constant string FES2022, on all four endpoints.
meta.datumMSL by default, else the uppercased code you requested (e.g. LAT). Always consistent with the values in the same response - the shift and the series share one snap resolution, so they can never diverge onto different grid nodes.
meta.datum_officialtrue only when the shift value came from an official national source. Computed FES values and the MSL default are always official: false - a computed value is never labelled official.
meta.cachecache or synth: whether the series came from the shared cache or fresh synthesis. Informational only - the numbers are identical either way for the same lattice.
meta.attributionHeights, extremes and current: a constant FES2022 (CNES/LEGOS/NOVELTIS/CLS/AVISO) string. Datums: an array - the same constant first, plus the attribution of each matched official source.
datums: referenceThe constant model_msl: every value in the response is metres relative to model mean sea level.
datums: datumsMap of code to {value_m, official, source?, approximate?}. MSL is always present as {value_m: 0, official: false}. approximate: true appears exactly on MHHWS, MLLWS, MHHWN and MLLWN (a spring-neap × diurnal-inequality approximation, not series-derived). Official values win over computed ones and are tagged with their source. In a non-tidal basin all computed tidal codes are suppressed - only MSL plus the official values remain.
datums: chart_datum{name, value_m, official, note?}: the official national chart datum where we hold one; else LAT (computed, FES2022) with official: false and a note; else an MSL fallback with a note. Never a computed value labelled official.
datums: tidal / form_factortidal is false only via a regional override (Baltic-style basins); form_factor is a number or null.
datums: meta.*meta.method is the constant 'FES2022 synth, 18.6yr nodal reduction'; meta.msl_bias_assumed is true exactly when an official value is referenced below local MSL without a measured tie to model MSL (the two are assumed equal); meta.region {id, source, license} is present exactly when a regional override matched; disclaimers always begins with the FES no-warranty / not-for-navigation line.

Two honest caveats. Whether a sample lands exactly on end depends on the serving path - the cache includes it, live synthesis stops just short - so rely on the lattice, not on the final sample sitting at end. And when the coastline worker is briefly unavailable the API stays up in a degraded mode: snapped_from_land: true with method: "nearest_water" and a grid.distance_km over 6 km is that mode announcing itself, not a bug.

Errors & retries

A 200 body is the data object directly - no wrapper. Every error, on every endpoint, is exactly:

{ "error": { "code": "bad_request", "message": "..." } }

Responses that reach the handler (200/400/404/422 and handler 5xx) carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (per key, per product), as does 429; the early auth gates (401/402/403) do not. Plans with a monthly quota also get X-Quota-Limit / X-Quota-Used. Responses over 1 KB gzip at the origin when you send Accept-Encoding: gzip.

HTTPCodeRetry-AfterMeaning
400bad_request-Parameter validation: lat/lon range, ISO start/end, interval 5..120, days 1..30, end must be after start, span over 366 days - or an unknown ?datum= for this location (the message points you to /v1/tides/datums).
401unauthorized-Missing or invalid API key (Bearer header or ?key=). Checked before everything else - a bad key gets 401 even on an unlaunched product.
402quota_exceeded-Monthly points quota used up; carries X-Quota-Limit / X-Quota-Used.
403product_not_enabled-Key valid, but your account does not include this endpoint's product. Every plan includes every product, so this only appears if a product was explicitly disabled on your account (or is still coming soon).
404no_data-Water was found but serving returned nothing: the grid is not loaded, synthesis produced no result, or (on /datums) the datum computation failed - the last can be transient, so one retry is reasonable there.
422no_ocean_tide-/v1/tides/*: the point is genuinely inland, or an enclosed non-ocean sea like the Caspian. Astronomical tides only exist in the sea.
422no_ocean_cell / no_cell-/v1/waters and /v1/heavens: more than 60 km from any grid cell.
429rate_limitedseconds to window resetPer-minute rate limit exceeded; also carries X-RateLimit-Limit.
500internal_error-Unhandled server error.
503coming_soon- (do not poll)The product is not launched yet (today: Lands). /v1/tides/* never returns this today.
503datum_unavailablenot set (yet)/v1/tides/* with ?datum=: the datum family could not be resolved right now - a transient compute failure, deliberately distinct from the 400 unknown-datum so a transient is never blamed on you. Retry after a short pause; no Retry-After header is currently sent.
503waters_error / heavens_error30/v1/waters and /v1/heavens: transient grid stall or a staggered worker reload - clears in seconds to tens of seconds. During a rollout two consecutive requests can straddle workers and see adjacent cycles for under a minute. Back off 30 s and retry; not an outage.
503not_yet_available-Server misconfiguration: the grid worker URL is not configured at all. An unreachable worker surfaces as waters_error/heavens_error instead - this code is not a back-off-and-retry situation.

The retry rule of thumb: honor Retry-After when present (429, and the transient 503s waters_error / heavens_error at 30 s); retry datum_unavailable after a short pause even though it does not carry the header yet; never poll coming_soon - it flips when the product launches, not before.