API documentation
One API, four products. Every endpoint lives under https://mareelearth.com/v1 and is served from Mareel's own pre-computed data: tides are synthesized on demand from harmonic constants; the marine forecast and the global weather are read from pre-baked global grids. Responses are JSON.
Products
| Product | Status | Endpoints | What it serves |
|---|---|---|---|
| Tides | Live | /v1/tides/*, /v1/stations | Tide heights, high/low extremes, current level, tidal datums - any ocean coordinate |
| Waters | Live | /v1/waters | Marine forecast: waves incl. swell partitions, wind, currents, SST, depth - 16 days for waves and wind, ~9 for swell partitions, ~7-8 for currents and SST |
| Heavens | Live | /v1/heavens | Global weather: temperature, wind, clouds, precipitation, CAPE - land & sea, finest over cities |
| Lands | Coming soon | /v1/lands/* | Land & terrain data |
Coming-soon products return 503 coming_soon on their keyed endpoints until launch.
Authentication
Create an API key from your dashboard (sign in → API keys → Create key). Keys look like mk_live_… and are shown once at creation. Pass the key on every request, preferably as a Bearer token:
curl -H "Authorization: Bearer mk_live_YOUR_KEY" \ "https://mareelearth.com/v1/tides/current?lat=58.0&lon=11.0"
Also accepted: an X-Api-Key header, or ?key= as a query parameter (avoid in logs). All endpoints are GET and CORS-open, so browser use works - but treat keys as secrets and prefer calling from your server.
Quickstart
Three requests, one key. Pick a language once; every example in the docs follows it.
# Current tide at a Swedish west-coast point curl -H "Authorization: Bearer mk_live_YOUR_KEY" \ "https://mareelearth.com/v1/tides/current?lat=58.0&lon=11.0"
# Marine forecast at the same point: waves + wind 16 days, currents + SST ~7-8 (coverage = last valid step per variable) curl -H "Authorization: Bearer mk_live_YOUR_KEY" \ "https://mareelearth.com/v1/waters?lat=58.0&lon=11.0"
# 10-day weather (temperature, wind, clouds, precipitation) for a city curl -H "Authorization: Bearer mk_live_YOUR_KEY" \ "https://mareelearth.com/v1/heavens?lat=59.27&lon=15.21"
Batch requests
Every point product has a /batch variant that takes up to 100 coordinates in one call as equal-length comma lists, and returns a per-point results array in input order. A point with no data carries a per-point error and never fails the whole call. One 100-point sweep replaces 100 single calls: it is billed in credits across the covered locations (see Plans & credits), but counts as one request against the per-minute rate limit. A batch stretches your rate, not your request count. How many batch calls a key may have in flight at once is the plan's In flight allowance (see Plans & credits).
# Marine forecast at three points in one call curl -H "Authorization: Bearer mk_live_YOUR_KEY" \ "https://mareelearth.com/v1/waters/batch?lat=59.3,58.9,57.7&lon=18.1,17.6,11.9"
{ "count": 3, "covered": 3, "results": [ {...}, {...}, {...} ] }| Endpoint | Per point |
|---|---|
/v1/tides/batch | Heights AND extremes together - the complete tide answer |
/v1/waters/batch | The full marine-forecast body |
/v1/heavens/batch | The full weather body |
The same query params as the single-point routes (vars=, days=/hours=, interval=, or the tide window + datum) apply to every point. Max 100 points per call; a transient point failure is retryable and sets a top-level Retry-After.
Field slices for map overlays
For a heat-map or animated weather layer, do not sweep a point endpoint cell by cell. Waters and Heavens each have a /field variant that returns a whole raster tile of one variable over a bounding box in a single call: a uniform cols×rows grid (max 512²) at one time step. By default each pixel is IDW-interpolated from the nearest cells (the same spatial interpolation as the point endpoint, with a circular mean for directions and nearest for categorical fields); pass interp=nearest for raw cells to GPU-interpolate yourself. Each pixel is a delivered value, billed at the same per-value rate as a point (a 256×256 slice is ~66 credits) - the win is the single call, not a cheaper unit price, so it replaces tens of thousands of point calls without underpricing them. pack=int16 returns a compact base64 array (scale/offset/nodata). See the Waters and Heavens pages.
# One tile of significant wave height over the Bay of Biscay curl -H "Authorization: Bearer mk_live_YOUR_KEY" \ "https://mareelearth.com/v1/waters/field?var=wave_height&bbox=-6,43,-1,47&cols=256&rows=256&pack=int16"
Time window, resolution & fields
You ask for exactly what you want and pay for exactly that. Set the window with days= or hours=, the resolution with interval= (minutes, default hourly), and the fields with vars=. Waters and Heavens return a uniform series at your interval across the window - interpolated from the baked model steps (hourly near-term, coarsening to 3-hourly, 6-hourly and 12-hourly further out), so there are no gaps. Forecast confidence naturally decreases with range; the long-range hours are interpolated for a clean series, not extra model detail. Pass interval=native to skip that and get the raw baked steps at their native cadence. Tides synthesise the exact value at any interval, with no horizon limit.
Plans & credits
Every tier includes every product (Tides, Waters, Heavens - and Lands when it launches) on one key. Usage is billed in credits: a read is one value - one location × one timestep × one field - and credits ≈ (requests × base + reads) / 1000, with every request costing at least 1 credit. So your credits scale with the data you pull: more fields, a longer window or a finer interval cost proportionally more, fewer cost less, and late forecast hours cost less because there is less real data there (billed on native density, not the interpolated count). A full forecast (16 days on Waters, 10 on Heavens) is about 2 credits, a single tide about 1, a 100-location batch about 186. Only answers are billed in full: a request the API rejects (4xx, for example a point with no data) costs 1 credit, and a 5xx on our side costs nothing. A batch call answers 200 with an error per failed point; one in which no point was delivered because of a failure on our side costs nothing either.
| Plan | Credits / month | Rate limit | In flight | API keys | Price |
|---|---|---|---|---|---|
| Free | 25,000 | 60 / min | 1 | 1 | $0 |
| Seedling | 150,000 | 120 / min | 2 | 2 | $7 / mo |
| Grove | 1,000,000 | 300 / min | 3 | 5 | $19 / mo |
| Colossal | 5,000,000 | 600 / min | 6 | 20 | $79 / mo |
The rate limit is per key, per product - a burst on one product never eats another's budget. Every response that reaches the handler (and every 429) carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds to a full bucket) so you can pace yourself before hitting a 429; a 429 also includes Retry-After. Every response also carries X-Quota-Used: the credits this calendar month has cost, this call included. X-Quota-Limit accompanies it while your plan has a monthly cap; an account without one has no limit header, and the used header keeps counting.
In flight. The In flight column is how many field calls, and separately how many batch calls, one key may have running at the same time per product (Waters and Heavens count separately, as the rate limit does). A further call answers 429 concurrency_limited with Retry-After: 1. A call counts from the moment it is admitted (after the key, product, rate and quota checks) until its response is returned; closing the connection ends it at once. Field slices additionally run in a bounded pool on each grid worker, separate from the point pool, so a burst of large slices never slows point or batch calls; when a pool is full the call waits up to two seconds for a slot and then answers 503 busy with Retry-After: 2 - on point and batch calls as well as on field calls. Higher allowances are available on request.
Errors
All errors share one envelope:
{ "error": { "code": "unauthorized", "message": "Missing or invalid API key. ..." } }| HTTP | Code | Meaning |
|---|---|---|
| 400 | bad_request | Missing or invalid parameters |
| 401 | unauthorized | Missing or invalid API key |
| 402 | quota_exceeded | Monthly quota used up - upgrade to continue |
| 403 | product_not_enabled | Your account does not include this product |
| 422 | no_ocean_tide / no_ocean_cell / no_cell | No data at this point (Tides/Waters: point is inland; Heavens: >60 km from any cell) |
| 429 | rate_limited | Too many requests - respect Retry-After |
| 429 | concurrency_limited | More field or batch calls in flight on one key and product than the plan allows (In flight in Plans & credits) - retry after Retry-After (1 s) |
| 503 | busy | A pool on the grid worker is full (field calls and point/batch calls have separate pools) - retry after Retry-After (2 s) |
| 503 | waters_error / heavens_error | A transient failure on our side (a stall, a worker reload, a transport fault) - retry after Retry-After (30 s) |
| 503 | coming_soon | Product not launched yet (Lands) |
Each product page lists its full catalog, including the codes only field slices use (unknown_var, landmask_unavailable). Retry on 429 and on a 5xx that carries Retry-After. A 4xx is your request, never a stall: it never carries Retry-After and retrying it unchanged cannot succeed. The message names what to fix, e.g. Invalid lon "undefined" (must be a number in -180..180).
Attribution & use
Tide data is derived from the FES2022 model (CNES/LEGOS/NOVELTIS/CLS, via AVISO); forecast data blends open model output from NOAA, ECMWF, DWD, ECCC and Copernicus. Keep the meta.attribution notices visible where you display the data - details on the attribution page. All values are modelled estimates provided as-is: not for navigation or safety-of-life decisions.
API changelog
Every change a client can observe on the keyed /v1 routes, newest first. Additive changes (new fields, new headers) need no client action; anything that alters an existing field, code or price is marked breaking and announced before it ships. Service state and incidents are on the status page.
| Date | Change | Action |
|---|---|---|
| 2026-10-19 | Interpolation: an interpolated hour (Waters and Heavens, point and batch) is now null when either baked step around it is null. Before, the value from the other side was carried across the interval: a variable continued up to one native interval (12 h in the new tail) past its coverage time, and an instant listed in coverage_gaps got its neighbour's value. Series now end at the coverage time and gaps stay empty. | If you read values past coverage, they are now null. Values inside coverage and at native steps are unchanged. |
| 2026-10-19 | Waters forecasts run 16 days (384 h) instead of 10. The baked time axis gains twelve 12-hourly steps after 240 h (105 steps instead of 93). A request without days= or hours= returns the full 16 days, hourly by default, and field slices accept t= up to 384 and step= up to 104. Wind and the total wave fields reach day 16; the swell partitions still end around day 9 and currents and SST around day 7-8, as coverage reports. A full-horizon request is billed on the native steps, so it still costs about 2 credits. Heavens stays at 10 days. | None. Pass days=10 to keep the old window length. |
| 2026-10-09 | Billing: a batch call in which no point was delivered because of a failure on our side (every failed point transient) now costs 0 credits, as a 5xx does. It cost the 1-credit floor because a batch answers 200. A batch whose points all failed for the caller's own reason still costs 1, like a 4xx. | None |
| 2026-09-24 | A batch envelope's Retry-After now follows its worst failed point: 2 when the only transient failures were busy, 30 when any point met waters_error or heavens_error, absent when every failure is permanent. It was 30 whenever any point failed transiently. | Honour the envelope's Retry-After rather than assuming 30 |
| 2026-09-24 | X-Quota-Used is now on every response, including accounts with no monthly cap, which previously got neither quota header. X-Quota-Limit still accompanies it only where a cap exists. Two error fixes on the same routes: a point call written with lng instead of lon now works (it was accepted and then answered with a transient error it could never get past), and an invalid request answers 400 bad_request with the reason instead of 503. | None; read X-Quota-Used without requiring X-Quota-Limit |
| 2026-09-18 | A 503 now carries the code the grid worker meant. A full pool answers busy with Retry-After: 2 on point and batch calls too, where it previously surfaced as waters_error / heavens_error with Retry-After: 30. Field slices also get their own deadline (15 s for one attempt, against 6 s for a point call) and are no longer retried for you when that deadline passes, so a large cold slice is answered rather than refused. | Treat 503 busy as the retry-in-2-s answer on every route, not only on /field |
| 2026-09-16 | The in-flight allowance is now a plan attribute (Free 1, Seedling 2, Grove 3, Colossal 6 field calls and as many batch calls at once), counted per key and per product (Waters and Heavens separately, as the rate limit is) instead of two for every key across products, and a closed connection ends a call at once, so an aborted call no longer holds its slot until the upstream read finished. Higher allowances on request. | None; Free keys drop from two to one in flight |
| 2026-09-10 | Per key, at most two field calls and two batch calls may be in flight at once; a further call answers 429 concurrency_limited with Retry-After: 1. Sequential clients are unaffected. | Honour Retry-After; keep field and batch calls sequential |
| 2026-09-10 | Field slices run in a bounded pool per grid worker, separate from point calls: when it is full a field call answers 503 busy with Retry-After: 2 instead of timing out. Point and batch calls keep their own pool. Retry on Retry-After; run field scans sequentially. | Honour Retry-After |
| 2026-09-10 | Field slices (/v1/waters/field, /v1/heavens/field) accept landmask=1: a pixel whose centre is land in the GEBCO classification the grid cells are built from (~2 km lattice) is nodata, independent of distance. The response echoes landmask. | Optional |
| 2026-09-10 | Field slices accept maxkm=<km>: a pixel whose nearest grid cell is farther than maxkm is nodata. It only tightens the 60 km default and is a distance gate, not a land mask (offshore cells sit on a 0.1 degree lattice, so keep it at 8 km or more there). The response echoes the effective gate as max_km. | Optional |
| 2026-09-06 | Billing: a 5xx response now costs 0 credits (it cost 1). A 4xx still costs 1; a 2xx is billed as before. X-Quota-Used reflects it. | None |
| 2026-09-06 | Keyless demo routes (/v1/demo/waters, /v1/demo/heavens) are now rate-limited per IP. The keyed routes are unchanged. | None for keyed clients; use a key for programmatic access |
| 2026-09-06 | New public status feed: /status and GET /api/status (availability per product, p95 latency, incidents; 60 s cache). | Optional |
| 2026-08-18 | Waters/Heavens point responses carry coverage_gaps (in-window gaps per variable); field slices carry gap: true when empty inside the window. | None |
| 2026-08-17 | Waters/Heavens point responses carry coverage (last valid step per variable); field slices carry var_last_valid. Bad coordinates answer 400 on every route (some answered 503). | None |