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

← All docs

Heavens API

Global weather for any coordinate on Earth - land, sea, cities, mountains - out to 10 days. Served from Mareel's own pre-baked population-tiered grid: ~2 km cells over cities, ~4 km over towns and coasts, ~11 km everywhere else (10 million cells, no gaps). A region-aware blend of the best open models: convection-permitting regional models (2.2-6.5 km) win where they cover; the ECMWF + GFS global backbone carries the full horizon everywhere. Heavens is the atmosphere; the sea state is the separate Waters product.

Endpoint

GET /v1/heavens

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 - inside a city that is typically ~1 km). The grid covers land and sea alike, so virtually every coordinate resolves; a point more than 60 km from any cell returns 422 no_cell.

Response

{
  "query":    { "lat": 59.27, "lon": 15.21 },
  "resolved": { "nearest_cell_km": 0.9, "cell": [59.26, 15.20],
                "grid_cycle": "2026-07-15T00:00:00+00:00", "steps": 93 },
  "variables": ["temperature_2m", "wind_speed", "cloud_total", "precipitation", ...],
  "steps": [
    { "time": "2026-07-15T00:00:00+00:00",
      "values": { "temperature_2m": 13.8, "dewpoint_2m": 11.2, "relative_humidity_2m": 84.0,
                  "wind_speed": 6.1, "wind_gust": 11.0, "wind_direction": 232.0,
                  "cloud_total": 55.0, "precipitation": 0.0, "precip_type": 0,
                  "cape": 120.0, "mslp": 1013.2, "visibility": 24.1, ... } },
    ...
  ],
  "meta": { "model": "pre-baked region-aware grid", "cache": "static-grid",
            "took_ms": 1.6, "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. cloud_ceiling is null under a clear sky (no cloud base to report), and snow variables are null where snow is not modelled (tropical ocean).

Variables

KeyUnitConventionPrimary sources
temperature_2m°C2 m air temperatureECMWF IFS · GFS · ICON-EU/D2 · HRRR regionally
dewpoint_2m°C2 m dewpointsame as temperature
relative_humidity_2m%2 m relative humiditysame as temperature
wind_speed / wind_gustknots10 m sustained / gustECMWF IFS · GFS · ICON-EU/D2 · HRRR regionally
wind_directiondegcoming fromsame as wind
cloud_total%total cloud coverECMWF IFS · GFS · ICON · HRRR
cloud_low / cloud_mid / cloud_high%layer cloud coverGFS · ICON · HRRR (ECMWF open data has no layer split)
cloud_ceilingmcloud-base height; null = unlimitedGFS · HRRR
precipitationmm/hrate, all models normalized from their native accumulationsECMWF IFS · GFS · ICON · HRRR
precip_typecodeinteger enum: 0 none · 1 rain · 2 snow · 3 freezing rain · 4 ice pellets; 0 whenever precipitation is served alongside and is 0GFS · HRRR
snow_depthmon the groundGFS · ICON · HRRR
swemmsnow water equivalentGFS · HRRR
capeJ/kgthunderstorm potentialECMWF IFS · GFS · ICON · HRRR
cinJ/kgconvective inhibition (negative = capped)GFS · HRRR
mslphPamean-sea-level pressureECMWF IFS · GFS · ICON · HRRR
visibilitykmsurface visibilityGFS · HRRR

Where several models cover a point, the finest genuinely-available source wins per variable and time step (a 2.2 km convection-permitting model beats the global one inside its footprint) - deterministic per cell, no averaging across resolutions. A handful of variables (cloud_ceiling, precip_type, swe, cin, visibility) are published only by the NOAA models, so they carry GFS/HRRR resolution everywhere.

Field invariants

The envelope is identical to Waters: a 200 body has exactly query, resolved, variables, steps and meta, passed through verbatim from the Heavens grid worker (the same serving code as Waters, a different bake). The full tapered 10-day horizon is always returned - there is no horizon param. Headers are the same everywhere: X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset (per key, per product) on responses that reach the handler and on 429 (the early auth gates 401/402/403 skip it), X-Quota-* when a quota is set, conditional gzip; keyed 200s carry no Cache-Control. Per field, this is what you may rely on:

FieldYou may rely on
query.lat / query.lonNumbers - your request coordinates echoed back exactly as parsed to float. Per-point; never null on a 200.
resolved.grid_cycleString, ISO 8601 UTC (+00:00) - the base time of the loaded Heavens bake; always === steps[0].time. Grid-global: one value per loaded grid, never per-cell or per-region, snapshotted once per request. Monotone non-decreasing per serving worker - but the workers hot-reload staggered during a publish, so consecutive requests can straddle workers and step back by exactly one cycle for under a minute, then it is monotone again. Heavens and Waters are separate bakes on separate workers: their grid_cycle values are unrelated and roll at different moments. Null only for a bake that has never been published.
resolved.published_at / bake_idpublished_at is the ISO 8601 UTC time this grid was PUBLISHED; bake_id the same instant as a compact monotonic id (YYYYMMDDTHHMM). Unlike grid_cycle (the base time, which only advances on a new-base bake), both change on EVERY publish including same-base re-bakes with fresher inputs - key your refresh cadence on published_at to catch every publication. 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 (2 decimals), great-circle km to the served cell center. Per-point. On a 200 it is 0..60 - the hard contract bound - though on the land+sea population-tiered grid (~11 km floor) a >60 km miss is practically unreachable, so real-world values are small. Never null on a 200.
resolved.cell[cell_lat, cell_lon] of the nearest grid-cell center, floats at 4 decimals. Per-point; never null on a 200. cell and nearest_cell_km are the only snap fields - there is no snapped_from_land.
resolved.stepsInteger ≥ 1, the count of timesteps in the loaded bake; always === steps.length. Grid-global per cycle - identical for every point on the same grid_cycle.
variablesArray of strings - the served variable keys. ?vars= preserves your request order filtered to keys that exist; unknown names are silently dropped (all-unknown gives variables: [] and empty values, still a 200); omitted means the full bake key list.
stepsArray of { time, values }, length === resolved.steps, ascending. The axis is tapered - hourly to +48 h, 3-hourly to +120 h, 6-hourly to +240 h - so spacing is not uniform: always read steps[].time rather than assuming an interval.
steps[].timeString, ISO 8601 UTC (+00:00); steps[0].time === resolved.grid_cycle; strictly increasing.
steps[].values.<var>Number rounded to 2 decimals, or null. Null means that variable has no data at that cell and step (for example a regional refiner's shorter horizon has ended and no fallback existed) - never zero-filled. 0 is always a real zero.
steps[].values.precip_typeInteger categorical code 0-4, never fractional - the enum is enforced server-side (baked nearest-neighbour, and serving cleans any older blended bake). When precipitation is served in the same response and is 0 at a step, precip_type is forced to 0 - a type is meaningless without precip. (Requesting precip_type alone skips that consistency pass.) null when unbaked at that step.
meta.*Same constants and semantics as Waters: model = "pre-baked region-aware grid"; cache = "static-grid"; took_ms = grid-side lookup milliseconds (2 decimals, diagnostic only - not end-to-end latency); disclaimers = one element, "Not for navigation or safety-of-life decisions; modelled estimate, as is."

In short: resolved.grid_cycle, resolved.steps and the time axis are properties of the loaded grid - identical for every point until the next bake swaps in - while nearest_cell_km, cell and the values are properties of your point. Anything not listed here is not part of the contract.

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
ECMWF IFS 0.25° (~27 km)global backbone, full horizon2× / day
NOAA GFS 0.25° (~27 km)global keystone - the only global source of cloud layers, precip type, visibility4× / day
DWD ICON-EU 6.5 kmEurope incl. Scandinavia, to +48 h4× / day
DWD ICON-D2 2.2 kmCentral Europe, to +48 h8× / day
NOAA HRRR 3 kmContinental US, to +18 hhourly

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). The regional models are deliberately short-range: beyond +48 h (Europe) / +18 h (US) every point hands over to the ~27 km global backbone. The grid's fine cells are real regional-model structure inside those footprints; elsewhere they are honest interpolation of the global models - Heavens aims to be the most detailed and most honest open-data forecast, not to invent resolution.

Errors & retries

All errors share the one envelope from the docs index: { "error": { "code": ..., "message": ... } }. The full Heavens catalog, including which responses carry a Retry-After header:

HTTPCodeWhen it firesRetry-After
400bad_requestlat or lon missing. A present-but-non-numeric coordinate is caught deeper: the grid's own 400 is retried as transient and surfaces as 503 heavens_error, not 400.-
401unauthorizedMissing or invalid API key.-
402quota_exceededMonthly quota exceeded - carries X-Quota-Limit / X-Quota-Used headers.-
403product_not_enabledThe key's account does not have the Heavens product enabled.-
422no_cellNearest grid cell more than 60 km away - practically unreachable on the land+sea grid, but contractually reserved. Never retried internally. Note the code is no_cell here vs no_ocean_cell on Waters.-
429rate_limitedPer-key rate bucket exhausted; carries X-RateLimit-Limit.seconds until the bucket refills
500internal_errorUnexpected server error.-
503coming_soonDormant today (Heavens is live) - fires for every key only if the product is switched off.-
503not_yet_availableThe Heavens grid worker is not configured on the serving host.-
503heavens_errorTransient grid failure after two 6 s tries with jittered backoff - a stall, a worker hot-reload, a transport error or an upstream 5xx.30

Only 429 rate_limited and 503 heavens_error carry Retry-After, and they are the only two worth retrying blindly. Honor the header: stalls clear in seconds to tens of seconds, and because the serving workers reload staggered during a publish, a 30 s retry lands on a worker that is serving. Everything else is deterministic - fix the request or the account, not the timing.

Try it without a key

/v1/demo/heavens?lat=..&lon=.. is the keyless showcase window (same grid, CDN-cached, no SLA) - fine for evaluation, not for production. Production traffic belongs on /v1/heavens with your key. Sun and moon times shown on the Heavens page are computed client-side (astronomy, not forecast data) and are not API variables.