# Climate Memory API — full reference > Generated from the same source as the HTML reference at https://developers.climatememory.com/reference. If this file and those pages ever disagree, it is a bug — they are rendered from one set of section definitions. - Weather and Climate host: `https://api.climatememory.com` - Degree Days host: `https://api.climatememory.com` - Authentication: send the header `X-API-Key: YOUR_KEY` on every request. - Every endpoint is a `GET`. No request bodies, no pagination, no sessions. - Time series are column-oriented: parallel arrays sharing one `time` index. - Units are SI and fixed: °C, mm, m/s, hPa, W/m², metres. - `null` means *not known*, never zero. - OpenAPI 3.1 schemas: `https://api.climatememory.com/v1/openapi-weather.json`, `https://api.climatememory.com/v1/openapi-degree-days.json`, `https://api.climatememory.com/v1/openapi-climate.json`, `https://api.climatememory.com/v1/openapi-geocoding.json`. ## Endpoint index | Path | Host | Scope | Credits | Does | | --- | --- | --- | --- | --- | | `/v1/geocode` | `api.climatememory.com` | any key | 0 | Resolve a place name to coordinates, elevation and time zone. | | `/v1/cities/search` | `api.climatememory.com` | any key | 0 | Older name search. Matches the ASCII name only, orders by population. | | `/v1/cities/nearest` | `api.climatememory.com` | any key | 0 | The nearest catalogued city to a coordinate. | | `/v1/attribution` | `api.climatememory.com` | none | 0 | The exact required wording, machine-readable. No key needed. | | `/v1/licensing` | `api.climatememory.com` | none | 0 | The licence table behind every dataset. No key needed. | | `/v1/forecast` | `api.climatememory.com` | meteo | 1 | Hourly and daily forecast for an arbitrary coordinate. | | `/v1/forecast/city/{country}/{slug}` | `api.climatememory.com` | meteo | 1 | Same response, resolved through the city catalogue. | | `/v1/current` | `api.climatememory.com` | meteo | 1 | Conditions right now for a coordinate, interpolated between model steps. | | `/v1/current/city/{country}/{slug}` | `api.climatememory.com` | meteo | 1 | The same, through the city catalogue. | | `/v1/fields` | `api.climatememory.com` | none | 0 | The same table as JSON, with units and which fields are default. No key needed. | | `/v1/models` | `api.climatememory.com` | none | 0 | Which models are live, what each covers, how fresh each is. No key needed. | | `/v1/probability` | `api.climatememory.com` | meteo | 1 | Percentiles and rain probabilities from the 51-member ECMWF ensemble. | | `/v1/air-quality` | `api.climatememory.com` | meteo | 1 | Particulates, ozone, NO₂, SO₂, CO and Saharan dust, from Copernicus CAMS. | | `/v1/marine` | `api.climatememory.com` | meteo | 1 | Wave height, direction and period from the ECMWF wave model, worldwide, to 10 days. | | `/v1/hydrology` | `api.climatememory.com` | meteo | 1 | River discharge from Copernicus GloFAS on a 0.05° river network, to 10 days. | | `/v1/degree-days` | `api.climatememory.com` | dju | 2, +1 per complete 365.25-day period in the span | Heating and cooling degree days for a coordinate. | | `/v1/degree-days?base=15,15.5,18,18.5,20` | `api.climatememory.com` | dju | same as one base | Up to 60 base temperatures in a single call, at the price of one. | | `/v1/degree-days/compare-methods` | `api.climatememory.com` | dju | 2, +1 per complete 365.25-day period in the span | All four methods over the same period, side by side. | | `/v1/methods` | `api.climatememory.com` | none | 0 | The method definitions and their presets. No key needed. | | `/v1/degree-days/city/{country}/{slug}` | `api.climatememory.com` | dju | 2, +1 per complete 365.25-day period in the span | The same, with the city's time zone and its urban heat island correction. | | `/v1/degree-days?elevation={metres}` | `api.climatememory.com` | dju | same | Lapse-rate correction from the cell's ground height to yours. | | `/v1/cells/resolve` | `api.climatememory.com` | dju | 1 | Which cell would answer for a point, and how far away it is. | | `/v1/degree-days/cell/{cell_id}` | `api.climatememory.com` | dju | 2, +1 per complete 365.25-day period in the span | Degree days for a named cell — no nearest-cell search, ever. | | `/v1/degree-days?breakdown={daily|weekly|monthly|yearly}` | `api.climatememory.com` | dju | same | Aggregate the same request to the period your contract settles on. | | `/v1/degree-days/monthly` | `api.climatememory.com` | dju | 2 | Twelve monthly totals for one calendar year, with no daily series. | | `/v1/coverage` | `api.climatememory.com` | none | 0 | What the archive holds: first and last day with data, and the axis it will grow into. No key needed. | | `/v1/degree-days?format=csv` | `api.climatememory.com` | dju | 4× the JSON call | The breakdown rows as a CSV file. Paid plans only. | | `/v1/historical` | `api.climatememory.com` | dju | (years + 1) × (variables ÷ 2), rounded down, min 1 | The reanalysis archive served raw: hourly weather back to 1950. | | `/v1/historical/city/{country}/{slug}` | `api.climatememory.com` | dju | (years + 1) × (variables ÷ 2), rounded down, min 1 | Identical, with the city's time zone and heat-island correction. | | `/v1/historical/variables` | `api.climatememory.com` | none | 0 | What the archive currently holds, with units, and which fields are derived. No key needed. | | `/v1/climate/daily` | `api.climatememory.com` | climate | 2, +1 per complete 365.25-day period in the span | Thirteen daily fields for any land coordinate, 1940 to last week. | | `/v1/climate/monthly` | `api.climatememory.com` | climate | 2, +1 per complete 365.25-day period in the span | The same fields aggregated by calendar month. | | `/v1/climate/summary` | `api.climatememory.com` | climate | 3, +1 per complete 365.25-day period in the span | Monthly normals, annual series, records, trends and Köppen — no span limit. | | `/v1/climate/normals` | `api.climatememory.com` | normals | 3 | A thirty-year mean over a WMO reference period. | | `/v1/climate/normals/compare` | `api.climatememory.com` | normals | 6 | Both WMO periods and the change between them, in one call. | | `/v1/climate/coverage` | `api.climatememory.com` | none | 1 | The run being served and the dates it spans. No key needed. | | `/v1/climate/fields` | `api.climatememory.com` | none | 1 | What this run can answer for, with units, and which fields are derived. No key needed. | | `/v1/climate/cells/resolve` | `api.climatememory.com` | climate | 1 | Which cell answers for a point, and how far it is. | | `/v1/climate/licensing` | `api.climatememory.com` | none | 0 | The licence terms for this archive. No key needed. | --- # Get started Three APIs, one key: weather forecasts from ECMWF and national high-resolution models, heating and cooling degree days, and 87 years of daily climate. This page is everything you need before the first call. The endpoints themselves are on the three reference pages. Web version: https://developers.climatememory.com/start ## First steps ### What this is Climate Memory serves three products from two hosts, on a single API key. | Product | Host | Answers | Reference | | --- | --- | --- | --- | | Weather | `https://api.climatememory.com` | What the weather will do — forecast, current conditions, ensembles, air quality, sea state, rivers | [Weather API](https://developers.climatememory.com/reference/weather#forecast) | | Degree days | `https://api.climatememory.com` | How much a building had to be heated or cooled — from 1950, anywhere | [Degree Days API](https://developers.climatememory.com/reference/degree-days#dd) | | Climate | `https://api.climatememory.com` | What is normal here — 87 years of daily climate, WMO normals, trends | [Climate API](https://developers.climatememory.com/reference/climate#climate) | Two hosts rather than one because degree days read a different archive with a different cost profile, and separating them lets one be slow without making the other slow. You do not have to care beyond copying the right base URL. > **Note:** **Every endpoint is a `GET`.** There is no request body anywhere in this API, no pagination cursor and no session. A call is a URL plus a header, which means you can test any of it in a browser address bar with a key in a query-string-free tool like `curl`, and cache any of it in front of us with no special handling. ### Your first call Three steps. The whole thing takes about a minute. ##### 1. Get a key Create an account at [developers.climatememory.com/signin](https://developers.climatememory.com/en/signin). The free plan takes no card, gives you 10 000 credits a month, and your key is on screen immediately. Keep it in an environment variable — the examples below all read `$API_KEY`. ``` export API_KEY="wd_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ``` ##### 2. Make the call **curl** ```bash curl -sH "X-API-Key: $API_KEY" \ "https://api.climatememory.com/v1/forecast?lat=48.85&lon=2.35&days=3" ``` **Python** ```python import os, httpx r = httpx.get( "https://api.climatememory.com/v1/forecast", params={"lat": 48.85, "lon": 2.35, "days": 3}, headers={"X-API-Key": os.environ["API_KEY"]}, timeout=30, ) r.raise_for_status() data = r.json() print(data["daily"]["temperature_2m_max"]) ``` **JavaScript** ```javascript const res = await fetch( "https://api.climatememory.com/v1/forecast?lat=48.85&lon=2.35&days=3", { headers: { "X-API-Key": process.env.API_KEY } }, ); if (!res.ok) { const { error } = await res.json(); throw new Error(`${error.code}: ${error.message}`); } const data = await res.json(); console.log(data.daily.temperature_2m_max); ``` **Go** ```go req, _ := http.NewRequest("GET", "https://api.climatememory.com/v1/forecast?lat=48.85&lon=2.35&days=3", nil) req.Header.Set("X-API-Key", os.Getenv("API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { return err } defer res.Body.Close() var out struct { Daily struct { TemperatureMax []float64 `json:"temperature_2m_max"` } `json:"daily"` } json.NewDecoder(res.Body).Decode(&out) ``` ##### 3. Read the answer Every series comes back **column-oriented** — parallel arrays sharing one `time` index, not a list of objects. See [response conventions](https://developers.climatememory.com/start#conventions) for why, and for how to read it. > **Note:** **Next:** the same call by city name is usually better — `https://api.climatememory.com/v1/forecast/city/fr/paris` carries the city's true elevation and time zone, which a bare coordinate cannot. See [forecast by city](https://developers.climatememory.com/reference/weather#forecast-city). ### Authentication Send your key in the `X-API-Key` header on every request. There is no OAuth flow, no bearer token to refresh and no signature to compute. ``` X-API-Key: wd_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ``` Keys are stored hashed. If you lose one it is **rotated, not recovered** — we cannot show it to you again, because we do not have it. Rotation is in the console and issues a new key immediately. ##### Scopes A key carries scopes, and a plan decides which. Calling an endpoint outside your scopes returns `403 scope_denied` — which is a plan problem, not a key problem, and the message says so. | Scope | Unlocks | | --- | --- | | `meteo` | The whole Weather API, including the extras | | `dju` | Degree days and the hourly history | | `climate` | The daily climate archive and its aggregates | | `normals` | WMO normals and the normals comparison | The geocoding endpoints require **none of them**. Any valid key resolves a place name, on any plan, because every product here needs a coordinate before it can answer anything. See [geocoding](https://developers.climatememory.com/start#geocode). > **Important:** **Never put your key in a browser.** Call the API from your server and pass the result to the client. A key in front-end JavaScript is a key anyone can read from the network tab and spend against your quota — and because it is your key, the usage is indistinguishable from yours. > > If you need weather in a mobile app, the mobile endpoints exist for exactly that: they issue a per-install token rather than embedding your key. ## Finding a place ### Geocoding - `GET https://api.climatememory.com/v1/geocode` — any key — 0 credits Resolve a place name to coordinates, elevation and time zone. | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `q` | string | — | Place name, in any language. Accents optional. Required. | | `lang` | ISO-639-1 | en | Display and ranking only — never what is eligible. An unknown code falls back to local names rather than failing. | | `near` | lat,lon | — | The caller's position. The strongest tiebreaker there is. | | `country` | ISO-3166-1 alpha-2 | — | Restrict to one country. | | `limit` | int | 10 | 1–50. | The catalogue is GeoNames feature class P *in full* — every populated place on earth, down to hamlets of a few dozen people, not a population-filtered extract. A village of 528 inhabitants is in it. > **Note:** **It costs no credits, on any plan, and needs no scope.** A key sold for degree days resolves names too, because degree days cannot be requested without a coordinate. It carries a ceiling of its own instead — five geocoding requests per credit in your plan — which is deliberately generous enough that a debounced type-ahead search box is the intended use. See [rate limits and quota](https://developers.climatememory.com/start#limits). ##### Matching is never restricted by language The query is tested against the local name, its ASCII transliteration and every localised alias, *whatever `lang` says*. That matters more than it sounds: 98 % of places have no localised name at all — French covers 1.84 % of the catalogue — so a language-filtered search finds capitals and nothing else. `lang` selects which name comes back and influences ranking; it never decides what is eligible. ``` GET /v1/geocode?q=ramillies&lang=fr GET /v1/geocode?q=ramillies&lang=fr&near=50.63,3.06 # from Lille GET /v1/geocode?q=bruxelles&lang=fr # exonym, via the alias index GET /v1/geocode?q=zuesch&lang=de # umlaut written out ``` ##### How results are ranked Each result carries a `score` and, when `near` was given, a `distance_km`. Ranking combines four signals: how well the name matched, how prominent the place is, how near it is to `near`, and whether the matched name was in the requested language. **Why `near` exists.** There are two Ramillies — 5 749 inhabitants in Walloon Brabant, 528 in the Hauts-de-France, 126 km apart. Neither is the right answer in the abstract. Without `near` the larger one is returned; from Lille, the French one is. **Prominence is not population alone.** 90.7 % of the catalogue has no population recorded — GeoNames publishes none whatsoever for some countries — so the place type (national capital, seat of an administrative division, ordinary place, neighbourhood) carries the ranking wherever population is absent. **Umlauts are written out, not dropped.** A German or Danish keyboard without diacritics writes `Zuesch` for `Züsch` and `Koeln` for `Köln`. GeoNames does not carry that spelling — its ASCII form is `Zusch`, with the umlaut removed rather than expanded — so it is generated here. Both spellings reach the place, and the result shows the real name either way. ### City lookup - `GET https://api.climatememory.com/v1/cities/search` — any key — 0 credits Older name search. Matches the ASCII name only, orders by population. - `GET https://api.climatememory.com/v1/cities/nearest` — any key — 0 credits The nearest catalogued city to a coordinate. `/v1/cities/search` predates `/v1/geocode` and keeps its contract for the callers already built on it: it matches the ASCII name only and orders by population. > **Note:** **Prefer [`/v1/geocode`](https://developers.climatememory.com/start#geocode) for anything user-facing.** The older route cannot match an exonym — *Bruxelles* will not find *Brussel* — and its ordering is undefined for the nine places in ten that have no population recorded. `/v1/cities/nearest` is the reverse direction, and is what turns a GPS fix into a city slug you can then pass to the city endpoints. It takes `lat`, `lon` and an optional `max_km`. Both count against the same geocoding ceiling as `/v1/geocode`, and neither costs credits. ## Contract ### Response conventions Six rules hold across every endpoint. Learn them once and the rest of this documentation is just field names. ##### 1. Series are columns, not rows Every time series is a set of parallel arrays sharing one `time` index, rather than an array of objects: ``` "hourly": { "time": ["2026-08-01T00:00:00+00:00", "2026-08-01T01:00:00+00:00"], "temperature_2m": [18.4, 18.1], "precipitation": [0.0, 0.2] } ``` Index `i` of every array describes the same instant. This is three to five times smaller on the wire than the row form, it decodes straight into a dataframe or a plotting library without a transform, and it is what Open-Meteo-shaped clients already expect. To walk it as rows: ``` rows = zip(h["time"], h["temperature_2m"], h["precipitation"]) ``` ##### 2. Times are ISO 8601 with an explicit offset Always. `2026-08-01T00:00:00+00:00`, never a bare local string and never a Unix integer. Where you pass a `timezone`, the offset in the response is that zone's — so the string alone is unambiguous and you never have to know what we assumed. ##### 3. Units are SI, fixed, and never negotiated °C, mm, m/s, hPa, W/m², metres. There is no `units=imperial` parameter, on purpose: a unit switch is a field whose meaning depends on another field, and it is how a caller ends up plotting Fahrenheit on a Celsius axis after a config change nobody reviewed. Convert at your edge, where the reader is. Endpoints that carry unusual units ship a `units` block in the response naming them explicitly, rather than expecting you to remember. ##### 4. `null` means "not known", never "zero" A missing hour is `null` in the array, holding its position so the indices stay aligned. It is never silently filled with 0 — for precipitation the two are opposite claims, and one of them is a lie about a drought. ##### 5. Every response says where it came from A `source` block (weather) or a `quality` block (archives) travels with the data: which model or run answered, how old it is, how complete the period was. You never have to infer freshness from the clock. ##### 6. Stale beats absent If our newest model run is older than expected, we still answer — with `"stale": true` and `data_age_hours` in the `source` block — rather than returning 503. A forecast eight hours old is more useful than an error. Check the flag if freshness is load-bearing for you; ignore it if it is not. > **Note:** **There is no pagination anywhere.** A request returns its whole answer or fails with a range error telling you the maximum. Spans are bounded per endpoint instead — which means retry logic never has to handle a half-read result set. ### Rate limits, credits and quota Three independent limits apply, and they fail differently on purpose. Every successful response reports where you stand: | Header | Meaning | | --- | --- | | `X-RateLimit-Remaining` | Requests left in the current sliding minute | | `X-Quota-Remaining` | Credits left this month | | `X-Quota-Resets-At` | ISO timestamp of the next monthly reset | | `X-Geocode-Remaining` | Geocoding requests left this month. Only on the catalogue endpoints, which are the only ones it bounds. | ##### Credits, not requests A cached city forecast is a 3 ms read. Ten years of hourly degree days across five hundred sites is not. Pricing in requests would let a caller sit inside their quota and cost more than they pay, entirely legitimately — so what a call costs depends on how much archive it moves. | Endpoint | Credits | | --- | --- | | `/v1/forecast`, `/v1/current`, and the city forms | 1 | | `/v1/probability`, `/v1/air-quality`, `/v1/marine`, `/v1/hydrology` | 1 | | `/v1/cells/resolve`, `/v1/climate/cells/resolve` | 1 | | `/v1/climate/normals` | 3 | | `/v1/climate/normals/compare` | 6 — it answers two periods | | `/v1/climate/daily`, `/v1/climate/monthly` | 2, +1 per complete 365.25-day period in the span | | `/v1/climate/summary` | 3, +1 per complete 365.25-day period in the span — **about 89 over the full archive** | | `/v1/degree-days` and its city and cell forms | 2, +1 per complete 365.25-day period in the span | | `/v1/degree-days/monthly` | 2 | | `/v1/historical` | (years + 1) × (variables ÷ 2), rounded down, minimum 1 | | Any of the above with `format=csv` | 4× the JSON cost | | `/v1/geocode`, `/v1/cities/search`, `/v1/cities/nearest` | **0** — bounded separately | | `/v1/models`, `/v1/methods`, `/v1/coverage`, `/v1/climate/coverage`, `/v1/climate/fields`, `/v1/historical/variables`, `/v1/attribution`, `/v1/licensing` | 1, and no key needed | > **Note:** **Geocoding costs no credits.** Every product here needs a coordinate before it can answer, so resolving a name is included rather than sold. A type-ahead search box is the intended use: debounce it and let it run. > > It is bounded, though, and by its own ceiling rather than by your credits: **five geocoding requests per credit in your plan**. On the 50 000 credit plan that is 250 000 searches a month, and a debounced search box spends around six of them per place found. Exhausting it returns `429 geocode_quota_exceeded` and leaves your credits untouched — which is also why the two are separate codes. ##### What to do when you hit each one | Limit | Response | Handling | | --- | --- | --- | | Rate (per minute) | `429 rate_limited` + `Retry-After` | Sleep for `Retry-After` seconds and retry. This one is transient by construction. | | Credits (per period) | `429 quota_exceeded` + `X-Quota-Resets-At` | Retrying will not help until the reset. Upgrade, or degrade your feature. | | Geocoding ceiling | `429 geocode_quota_exceeded` | Your credits are intact and every other endpoint still works. Debounce harder. | Your quota period runs from your subscription date to the same day of the following month — the period Stripe invoices — and the counter starts again whole each time. `X-Quota-Resets-At` carries that date on every response, so read it rather than assuming the first of the month. Accounts with no subscription follow the calendar month. > **Note:** **Cache aggressively — we do not mind, and it is free quota.** A forecast run changes 2 to 4 times a day; a degree-day total for a past month never changes at all. Nothing here is user-specific, so an ordinary HTTP cache in front of us is safe. The archive endpoints in particular are worth caching forever below the last five days. ### Errors Every error has the same shape, on every host: ``` { "error": { "code": "range_too_long", "message": "Requested span is 14.0 years; the maximum per request is 10.", "max_years": 10 } } ``` > **Important:** **Build your retry logic on `error.code`, not on the HTTP status.** Three different conditions return `429` and they need three different reactions: one wants a short sleep, one wants a plan change, and one should not stop your application at all. The status alone cannot tell them apart. Extra keys — `max_years` above — carry the limit you crossed, so your client can adapt rather than guess. | Status | Code | What to do | | --- | --- | --- | | 400 | `invalid_latitude`, `invalid_longitude` | Fix the coordinates. Latitude is −90…90, longitude −180…180. | | 400 | `range_too_long` | Split into several requests. `max_years` says the cap. | | 400 | `bad_request` | The generic form, when a check has no more specific code of its own. Treat it as permanent — the request will not become valid by being retried. | | 400 | `invalid_coordinates` | The pair is out of range or not a point on Earth. | | 400 | `invalid_method`, `invalid_base` | See [calculation methods](https://developers.climatememory.com/reference/degree-days#dd-methods). | | 400 | `invalid_breakdown` | Not one of `daily`, `weekly`, `monthly`, `yearly`. | | 400 | `invalid_date` | A date that is not ISO `YYYY-MM-DD`. | | 400 | `invalid_range` | `end` precedes `start`. | | 400 | `invalid_near` | `near` is not `lat,lon`. | | 400 | `unknown_field`, `unknown_variable` | Ask `/v1/fields`, `/v1/climate/fields` or `/v1/historical/variables` what that product holds — none of the three needs a key. On `/v1/forecast` the message names the closest match to what you asked for. | | 400 | `unknown_period` | Not a WMO reference period. See [normals](https://developers.climatememory.com/reference/climate#climate-normals). | | 400 | `period_not_covered` | The archive does not span that reference period at this point. | | 401 | `missing_api_key` | The `X-API-Key` header was absent. | | 401 | `invalid_api_key` | The key is unknown or rotated. | | 401 | `unauthorized` | The generic form, when nothing more specific applies. | | 402 | `export_not_in_plan` | CSV export is a paid-plan feature. | | 403 | `scope_denied` | Your plan does not include this API. See [scopes](https://developers.climatememory.com/start#auth). | | 403 | `key_inactive` | The key exists but has been revoked or disabled. Rotating a key does this to the old one — check the console before assuming an outage. | | 404 | `city_not_found` | The response includes a `suggestions` array — show it. | | 404 | `cell_not_found` | A pinned cell that no longer exists. See [pinning a cell](https://developers.climatememory.com/reference/degree-days#dd-pinned). | | 404 | `not_at_sea` | A marine request over land. The wave model has no value there. | | 404 | `not_found` | The generic form. As above: permanent. | | 404 | `no_city_nearby` | Nothing catalogued within `max_km` of that point. | | 404 | `outside_archive` | The dates are outside what the archive holds. `/v1/climate/coverage` says what it holds, and needs no key. | | 429 | `rate_limited` | Back off for `Retry-After` seconds. | | 429 | `quota_exceeded` | Wait for the reset or upgrade. Retrying sooner cannot succeed. | | 429 | `geocode_quota_exceeded` | The geocoding ceiling only. Your credits are intact; debounce your search box. | | 503 | `data_unavailable` | Transient. Retry with exponential backoff. | | 503 | `load_shed` | Deliberately refused to protect the service under load. Your plan's criticality decides who is shed first. Retry with backoff; it clears in seconds. | | 503 | `catalogue_unavailable` | The city catalogue is briefly out of reach. Coordinates still work — fall back to them rather than failing the request. | That is the whole list, and it is checked against the source on every build: a code published here that no handler raises fails the test suite, and so does a handler raising one this table does not carry. If you meet a code that is not above, it is a bug in this page and not a value you should special-case. ##### A retry policy that works ``` RETRY = {"rate_limited", "data_unavailable"} def should_retry(status, code, attempt): if code == "quota_exceeded": return False # nothing changes until the reset if code == "geocode_quota_exceeded": return False # and do not stop: credits still work if code in RETRY: return attempt < 5 return status >= 500 ``` ## Before you commit ### Accuracy — what to expect We would rather set expectations correctly than have you discover the limits in production. ##### Where the forecast is strong Temperature, pressure, synoptic wind, humidity, sunshine. Typical 2 m temperature error at 24 hours is around 1–1.5 °C. ECMWF is the best global deterministic model in the world and it is what you are getting. ##### Where it is weaker Convective storms — a 28 km cell cannot resolve a thunderstorm, and even 3 km only hints at one. Exact placement and timing of showers. Coastal and mountain microclimate below the model's resolution. Sea breezes and valley cold pools, which the terrain correction does not model. ##### Geographic honesty In France, Germany, the USA and Canada we serve 1.3–3 km national models, and a small village genuinely gets its own grid cell. Elsewhere in Europe you get 6.5 km. Across Africa, the Middle East and most of Asia the best available public model is 13–28 km, so you get the weather of the area rather than of the street. The terrain correction narrows that gap but does not close it. ##### Degree days Gridded reanalysis covers everywhere, including locations with no weather station within 60 km — that is its advantage over station-based providers. Its weakness is city centres, which reanalysis under-reads; the city endpoints apply a calibrated correction, coordinate endpoints do not. ##### Two fields are estimates, and we would rather say so `uv_index` is derived from solar elevation and broadband radiation, not from an ozone column — accurate to roughly ±1 unit. `precipitation_probability_proxy` is an estimate, not a probability; the genuine one is at [/v1/probability](https://developers.climatememory.com/reference/weather#probability). Both are explained in full in the [field reference](https://developers.climatememory.com/reference/weather#fields). ### Plans | Plan | Price | Credits / month | Rate | Keys | Scopes | | --- | --- | --- | --- | --- | --- | | **Free** | €0 | 10 000 | 20 / min | 1 | weather, plus degree days and climate for 30 days | | **Starter** | €19 / month | 50 000 | 60 / min | 3 | weather + degree days + climate | | **Pro** | €79 / month | 400 000 | 300 / min | 10 | weather + degree days + climate + normals | | **Business** | from €299 / month | Agreed in contract | Agreed in contract | 50 | weather + degree days + climate + normals | Geocoding sits outside this table: it costs no credits on any plan, and carries its own ceiling of five requests per credit. See [rate limits and quota](https://developers.climatememory.com/start#limits). Annual billing is two months free. The live catalogue is at [developers.climatememory.com/pricing](https://developers.climatememory.com/en/pricing) — that page reads the billing system directly, so if the two ever disagree, it wins. There is no bulk export endpoint on any plan. CSV export exists on the degree-day and climate archives, is bounded to the same span as the equivalent JSON call, and returns aggregated rows only — never the hourly series. ### Attribution and licensing - `GET https://api.climatememory.com/v1/attribution` — no key needed — 0 credits The exact required wording, machine-readable. No key needed. - `GET https://api.climatememory.com/v1/licensing` — no key needed — 0 credits The licence table behind every dataset. No key needed. > **Important:** **Attribution is a condition of use, not a courtesy.** The underlying data is licensed to us under terms that require it, and those terms pass to you. Display a notice wherever the data appears, including in derived products. A footer line is enough: > > *Weather data: ECMWF, DWD, NOAA, Météo-France · Climate data: Copernicus/ERA5-Land · Places: GeoNames* Both endpoints above need no key, so you can render the notice from the API rather than hard-coding a string that goes stale when a source changes. The attribution for the specific run that answered also travels on every data response, in `source.attribution` and in the `X-Data-Attribution` header. You may state that you use this data. You may **not** imply that any of these organisations produced, approved or endorses your product. ##### Disclaimer Forecasts are provided without warranty. Neither the European Commission, ECMWF, DWD, NOAA nor Météo-France is responsible for any use made of this information. Do not use it as the sole basis for decisions where life, safety or property are at risk. One restriction is not ours to waive: the Copernicus licence reserves flood warnings to national and regional authorities. See [rivers](https://developers.climatememory.com/reference/weather#hydrology). --- # Weather API Global forecasts to 15 days from ECMWF and the national high-resolution models, terrain-corrected to your real ground elevation. One scope — `meteo` — covers this whole page. Web version: https://developers.climatememory.com/reference/weather ## Core ### Forecast - `GET https://api.climatememory.com/v1/forecast` — scope `meteo` — 1 credit Hourly and daily forecast for an arbitrary coordinate. | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `lat` | float | — | Latitude, −90 to 90. Required. | | `lon` | float | — | Longitude, −180 to 180. Required. | | `days` | int | 7 | 1–16 is accepted, but the served horizon is 15 days, so a 16 returns 15. Days 11–15 are a trend, not a forecast — see below. | | `elevation` | float (m) | — | True ground elevation of your point. Supply it and the temperature is corrected for the difference against the model's smoothed terrain. Typically worth 1–3 °C in hills. | | `timezone` | IANA zone | UTC | The zone daily aggregates are cut on, and the offset every timestamp carries back. | | `hourly` | csv | all | Subset of hourly fields, to shrink the payload. Names are those in the field reference. | | `include_daily` | bool | true | Include the daily block. | ##### Response ``` { "latitude": 48.85, "longitude": 2.35, "timezone": "Europe/Paris", "grid": { "latitude": 48.85, "longitude": 2.35, "distance_km": 0.42, "resolution_km": 1.3 }, "elevation": { "applied": true, "model_elevation_m": 91.0, "location_elevation_m": 113, "temperature_offset_c": -0.14 }, "source": { "model": "mf_arome", "model_run": "20260801T00Z", "reference_time": "2026-08-01T00:00:00+00:00", "data_age_hours": 3.95, "stale": false, "attribution": "Data: Météo-France (etalab-2.0)" }, "hourly": { "time": ["2026-08-01T00:00:00+02:00", "..."], "temperature_2m": [18.1, 17.9, 17.6, "..."], "apparent_temperature": [17.4, "..."], "weather_code": [0, 1, 2, "..."] }, "daily": { "date": ["2026-08-01", "..."], "temperature_2m_max": [26.6, "..."], "temperature_2m_min": [16.2, "..."], "precipitation_sum": [0.0, "..."], "uv_index_max": [7.6, "..."], "sunrise": ["2026-08-01T06:24:00+02:00", "..."], "sunset": ["2026-08-01T21:24:00+02:00", "..."], "daylight_hours": [15.0, "..."] } } ``` > **Note:** **The `grid` block is worth reading.** `distance_km` tells you how far the model cell centre is from the point you asked for, and `resolution_km` how coarse that cell is. Together they tell you how literally to take the numbers: 0.4 km from a 1.3 km cell is *your street*; 12 km from a 28 km cell is *your region*. > **Note:** **The `elevation` block says what was done, not what was asked.** `applied: false` means no correction happened — either you passed no `elevation`, or the model's terrain already matched. Never assume the correction ran because you requested it. ##### Shrinking the payload The full hourly block for 15 days is around 32 fields × 360 hours. If you plot three of them, ask for three: ``` GET /v1/forecast?lat=48.85&lon=2.35&days=3 &hourly=temperature_2m,precipitation,weather_code &include_daily=false ``` Same price — the cost is reading the archive, not serialising it — but a tenth of the bytes and noticeably faster to parse on a phone. ### Forecast by city - `GET https://api.climatememory.com/v1/forecast/city/{country}/{slug}` — scope `meteo` — 1 credit Same response, resolved through the city catalogue. **Prefer this over raw coordinates where you can.** The catalogue supplies two things a coordinate cannot: the city's **true elevation**, so the terrain correction applies without you supplying it, and its **time zone**, so daily aggregates are cut on the right local day. ``` GET /v1/forecast/city/dz/alger # French slug GET /v1/forecast/city/dz/algiers # English slug — same city GET /v1/forecast/city/fr/paris ``` `country` is an ISO-3166-1 alpha-2 code, lower case. `slug` is accent-insensitive: `bejaia` finds Béjaïa. An unknown slug returns `404 city_not_found` with a `suggestions` array — show it rather than a dead end. All the query parameters of [/v1/forecast](https://developers.climatememory.com/reference/weather#forecast) still apply except `lat`, `lon` and `elevation`, which the catalogue provides. Passing `timezone` overrides the city's own, which is almost never what you want. > **Note:** **Do not hard-code slugs you guessed.** Resolve the name once through [/v1/geocode](https://developers.climatememory.com/start#geocode), store the country and slug it returns, and use those. Geocoding costs no credits, so this is free to do properly. ### Current conditions - `GET https://api.climatememory.com/v1/current` — scope `meteo` — 1 credit Conditions right now for a coordinate, interpolated between model steps. - `GET https://api.climatememory.com/v1/current/city/{country}/{slug}` — scope `meteo` — 1 credit The same, through the city catalogue. Temperature, humidity, wind and pressure are interpolated linearly to the current instant, between the two model steps that bracket it. > **Important:** **Precipitation is not interpolated, and that is deliberate.** An hourly precipitation value is a total over an interval, not a reading at an instant. Interpolating it would invent rain in a minute the model placed in a different hour. You get the containing step's value instead — a real number about a real interval. Use this for a "right now" display. For anything you will compare across time, use [/v1/forecast](https://developers.climatememory.com/reference/weather#forecast) and read the hour you want: the series is stable, whereas *now* moves under you between two calls. ### Field reference - `GET https://api.climatememory.com/v1/fields` — no key needed — 0 credits The same table as JSON, with units and which fields are default. No key needed. #### Hourly fields 21 of these come back by default. The rest are yours for the asking — name them in `hourly=`, comma-separated — and they are held back for cost, not for doubt: each stored field is a separate compressed read, so a default response carrying all 45 would make every caller pay for the few who want snow depth. ``` GET /v1/forecast?lat=45.19&lon=5.72&hourly=temperature_2m,snow_line_altitude,soil_temperature_8cm ``` A field a model does not publish comes back as `null` for that model's stretch of the forecast rather than vanishing from the response — so a chain that starts on AROME and continues on ICON-EU returns `visibility` throughout, null for the first 51 hours. An **unknown** field name is a `400 unknown_field`, with the closest match suggested; it used to be silently ignored. | Field | Unit | Notes | | --- | --- | --- | | `temperature_2m` | °C | Terrain-corrected when elevation is known | | `apparent_temperature` | °C | Steadman formulation; valid across the whole range | | `relative_humidity_2m` | % | | | `dewpoint_2m` | °C | | | `precipitation` | mm | Total over the step, de-cumulated | | `precipitation_probability_proxy` | % | Heuristic, not an ensemble probability. See below. | | `weather_code` | WMO | Icon code, thresholded on mm/h not on the step total | | `cloud_cover` | % | | | `wind_speed_10m` | m/s | | | `wind_direction_10m` | ° | Direction the wind blows *from* | | `wind_gust_10m` | m/s | Maximum over the step | | `pressure_msl` | hPa | Reduced to sea level | | `surface_pressure` | hPa | At the location's elevation | | `shortwave_radiation` | W/m² | Mean over the step | | `uv_index` | 0–11+ | Estimated. See below. | | `cape` | J/kg | Thunderstorm potential | | `is_day` | 0/1 | | | `visibility` | m | ICON-EU and ICON-D2 only | | `cloud_cover_low` | % | ICON models only | | `cloud_cover_mid` | % | ICON models only | | `cloud_cover_high` | % | ICON models only | | `precipitation_type` | WMO | ECMWF only *Ask for it in `hourly=`* | | `snow_depth` | m | What is lying, not what falls. ICON only *Ask for it in `hourly=`* | | `snow_water_equivalent` | mm | What the snowpack yields on melting. ICON only *Ask for it in `hourly=`* | | `snow_line_altitude` | m | **See [snow](https://developers.climatememory.com/reference/weather#snow).** ICON-EU and ICON-D2 *Ask for it in `hourly=`* | | `freezing_level_altitude` | m | 0 °C isotherm. ICON only *Ask for it in `hourly=`* | | `soil_temperature_0cm` | °C | Surface. ICON global and ICON-EU only *Ask for it in `hourly=`* | | `soil_temperature_8cm` | °C | Root zone, 7–28 cm layer. ICON global and ICON-EU only *Ask for it in `hourly=`* | | `shortwave_radiation_direct` | W/m² | Beam component. ICON only *Ask for it in `hourly=`* | | `shortwave_radiation_diffuse` | W/m² | Sky component. ICON only *Ask for it in `hourly=`* | | `solar_elevation` | ° | Computed from the timestamp and the coordinate *Ask for it in `hourly=`* | | `solar_azimuth` | ° | Computed from the timestamp and the coordinate *Ask for it in `hourly=`* | | `temperature_2m_max` | °C | Over the step, where the model publishes it *Ask for it in `hourly=`* | | `temperature_2m_min` | °C | Over the step, where the model publishes it *Ask for it in `hourly=`* | | `skin_temperature` | °C | Ground surface, not air *Ask for it in `hourly=`* | | `heat_index` | °C | US NWS. Returns plain temperature below 27 °C *Ask for it in `hourly=`* | | `wind_chill` | °C | Environment Canada. Returns plain temperature above 10 °C *Ask for it in `hourly=`* | | `wind_speed_100m` | m/s | Wind-energy applications *Ask for it in `hourly=`* | | `wind_direction_100m` | ° | *Ask for it in `hourly=`* | | `wind_beaufort` | 0–12 | *Ask for it in `hourly=`* | | `cloud_cover_octas` | 0–8 | *Ask for it in `hourly=`* | | `snowfall` | mm | Water equivalent, not depth of fresh snow *Ask for it in `hourly=`* | | `total_column_water_vapour` | kg/m² | *Ask for it in `hourly=`* | | `pressure_tendency` | hPa | *Ask for it in `hourly=`* | | `weather_description` | text | *Ask for it in `hourly=`* | #### Daily fields `date`, `temperature_2m_max`, `temperature_2m_min`, `temperature_2m_mean`, `precipitation_sum`, `wind_speed_max`, `wind_gust_max`, `shortwave_radiation_sum` (MJ/m²), `uv_index_max`, `sunrise`, `sunset`, `daylight_hours`, `weather_code`. Aggregated on **local** calendar days, not UTC. A partial day at either end of the range is omitted rather than reported with a misleading maximum — so a 7-day request can legitimately return 6 daily rows. > **Important:** **Two fields are estimates, and we would rather say so than have you find out.** > > `uv_index` is derived from solar elevation and broadband radiation, not from an ozone column. Accurate to roughly ±1 unit — enough for "wear a hat", not for a medical claim. > > `precipitation_probability_proxy` is an estimate, not a probability. A deterministic model has no spread to derive one from. It is named `_proxy` so nobody mistakes it for the ensemble product served by [`/v1/probability`](https://developers.climatememory.com/reference/weather#probability), which is a genuine frequency over 51 members. > > It combines two terms. The first asks whether the cell's rain reaches your point: the stored rate is an average over the whole grid cell, so at 28 km — 780 km² — a light mean may be a real shower over a small part of it. Taking the sub-grid rate distribution as Weibull, with a shape set by the cell size and the model's own mean, gives the chance a point in the cell exceeds 0.1 mm/h in closed form. The second term bounds what an overcast, near-saturated sky may claim where the model puts no rain in the cell at all. Both are then scaled by how much a run deserves to be believed at that lead time, which is why the same rainfall reads lower on day 9 than on day 1. > > Consequently **the same place can read differently from two models, and should**: a 2.2 km cell and a 28 km cell genuinely disagree about what a light average means. Consumer apps that quote one number hide this. ### Models and resolution - `GET https://api.climatememory.com/v1/models` — no key needed — 0 credits Which models are live, what each covers, how fresh each is. No key needed. We serve the finest model that covers your point *and* your lead time. You never choose one; the choice is reported in `source.model` so you can always tell which answered. High-resolution models are all short-range — at 1 km the atmosphere becomes chaotic within two days, so forecasting further would be meaningless — and the resolution therefore steps down as the forecast advances: ``` day 0 ─────── day 2 ─────── day 5 ───────────────── day 15 AROME 1.3 km (France) ICON-D2 2.2 km (Germany, Alps, Benelux) HRRR 3 km (USA and southern Canada) ICON-EU 6.5 km (Europe) ICON 13 km · ECMWF 28 km (global) AIFS (trend only) ``` | `source.model` | Model | Resolution | Runs | Horizon | | --- | --- | --- | --- | --- | | `mf_arome` | Météo-France **AROME** | 1.3 km | 8/day | 51 h | | `dwd_icon_d2` | DWD **ICON-D2** | 2.2 km | 4/day | 48 h | | `noaa_hrrr` | NOAA **HRRR** | 3 km | 4/day | 48 h | | `dwd_icon_eu` | DWD **ICON-EU** | 6.5 km | 2/day | 120 h | | `dwd_icon` | DWD **ICON** | 13 km | 2/day | 180 h | | `ecmwf_ifs` | ECMWF **IFS** | 28 km | 2/day | 240 h | | `ecmwf_aifs` | ECMWF **AIFS** | 28 km | 2/day | 360 h | | `ecmwf_wave` | ECMWF **wave** | 28 km | 2/day | 240 h | Transitions across a model's domain edge are cross-faded, so two towns either side of a boundary never disagree by a step change. > **Note:** **Days 11–15 are a trend, not a forecast.** At that range skill approaches climatology. We publish it because it is asked for; treat it as direction of travel, and if you display it, say so. `/v1/models` needs no key, which makes it the right thing to poll from a status page or to check before you buy: it reports each model's latest run and its age, so "is the data fresh" is answerable without spending a credit. ## Extras ### Ensemble probabilities - `GET https://api.climatememory.com/v1/probability` — scope `meteo` — 1 credit Percentiles and rain probabilities from the 51-member ECMWF ensemble. A single forecast says *22 °C on Thursday*. That is a guess presented as a fact. This endpoint answers the question you actually decide on: how sure is it, and what is the chance of rain worth planning around. It is computed from ECMWF's ensemble — the same model run 50 times from slightly different starting conditions. Where the runs agree, the forecast is confident. Where they scatter, the atmosphere itself is uncertain, and no model however good can tell you more. ``` { "members": 50, "hourly": { "time": ["2026-08-01T00:00:00+00:00"], "temperature_2m_p10": [26.4], "temperature_2m_p50": [26.78], "temperature_2m_p90": [27.3], "temperature_2m_spread": [0.4], "precipitation_probability_0_1mm": [10.0], "precipitation_probability_1_0mm": [0.0], "precipitation_probability_5_0mm": [0.0], "precipitation_probability_10_0mm": [0.0], "precipitation_p90": [0.1] } } ``` ##### How to read it `p10` and `p90` bracket the middle 80 % of the members: one in ten expects it colder than `p10`, one in ten warmer than `p90`. A `spread` of 0.4 °C is a settled situation you can trust; 3 °C means the models disagree and you should say so to your users rather than pick one. The rain thresholds are decisions rather than round numbers — 0.1 mm is *wet at all*, 1 mm is *take a coat*, 5 and 10 mm are *this is a problem*. `precipitation_p90` is the bad case: only one member in ten is wetter. > **Note:** Two variables only, temperature and precipitation. Adding cloud, wind and pressure would double the bandwidth of the whole platform for numbers nobody makes a decision on. ### Air quality - `GET https://api.climatememory.com/v1/air-quality` — scope `meteo` — 1 credit Particulates, ozone, NO₂, SO₂, CO and Saharan dust, from Copernicus CAMS. This is the endpoint that matters most across North Africa and the Mediterranean. A dust episode takes PM10 past a thousand micrograms per cubic metre for days at a time, which is a health decision rather than a number, and it is poorly covered by free consumer services. ``` { "current": { "pm2_5": 38.7, "band": "poor", "who_guidelines_ug_m3": {"pm2_5": 15.0, "pm10": 45.0, "nitrogen_dioxide": 25.0, "ozone": 100.0} }, "hourly": { "time": ["2026-08-01T00:00:00+00:00"], "pm2_5": [38.7], "pm10": [85.8], "ozone": [43.4], "nitrogen_dioxide": [19.0], "dust_aod_550nm": [0.24] }, "units": {"pm2_5": "ug/m3", "pm10": "ug/m3", "dust_aod_550nm": "1"} } ``` All concentrations are in **µg/m³**, the unit air quality is quoted in everywhere. `dust_aod_550nm` is an optical depth and has no unit: above about 0.5 the sky is visibly hazy, above 1.0 the sun is dimmed. The WHO short-term guideline values ride along in every response so a number can be placed without looking it up. `band` follows the European air quality index for PM2.5: good, fair, moderate, poor, very poor, extremely poor. ### Snow and the snow line Four fields, and they answer different questions. `snowfall` is how much falls; the rest describe what is on the ground and where. **Ask for them by name** — they are not in the default response: ``` GET /v1/forecast?lat=45.19&lon=5.72&hourly=snowfall,snow_depth,snow_water_equivalent,snow_line_altitude,freezing_level_altitude ``` | Field | Unit | What it tells you | | --- | --- | --- | | `snow_depth` | m | What is lying. Twenty centimetres can fall and melt, or land on eighty already there. | | `snow_water_equivalent` | mm | What it yields on melting. Half a metre of powder and half a metre of packed snow are very different. | | `snow_line_altitude` | m | **The altitude above which precipitation falls as snow.** | | `freezing_level_altitude` | m | Height of the 0 °C isotherm, typically a few hundred metres above the snow line. | **The snow line is the one worth reading.** "It rains at 800 m and snows at 1200" is a decision a ski resort, a road authority or a driver can act on; "3 mm of precipitation" is not. It matters in North Africa too — the Atlas has ski areas at Chréa and Tikjda, and the high plateaux at Sétif, Batna and Djelfa sit above 1000 m. > **Note:** Glaciers report tens of metres of snow depth, because that is how the model represents permanent ice rather than a measurement error. The deepest seasonal snow on earth is around 11 m — treat anything past that as ice, not weather. These come from DWD's ICON, which covers the world at 13 km. ECMWF's open data publishes snow depth but not the snow line, so this is one place where the coarser model is the more useful one. `snow_line_altitude` is a regional product: ICON-EU and ICON-D2 publish it, ICON global does not, so outside Europe you get the freezing level and the depth but not the line itself. ### Marine — sea state - `GET https://api.climatememory.com/v1/marine` — scope `meteo` — 1 credit Wave height, direction and period from the ECMWF wave model, worldwide, to 10 days. ``` { "hourly": { "time": ["2026-08-01T00:00:00+00:00"], "wave_height": [0.32], "wave_direction": [264.3], "wave_period": [3.79], "wave_peak_period": [4.21] } } ``` **Wave height is the significant height** — the mean of the highest third of the waves, which is roughly what an observer at sea reports. Individual waves reach about twice it, which is the number that matters if you are deciding whether to go out. Direction is where the waves come *from*, like wind direction. > **Note:** A point on land returns `404 not_at_sea` rather than a list of nulls. The wave model has no value over land by construction, and saying so is more useful than a response that reads like a service failure. If you let users drop a pin, handle this code explicitly. ### Rivers - `GET https://api.climatememory.com/v1/hydrology` — scope `meteo` — 1 credit River discharge from Copernicus GloFAS on a 0.05° river network, to 10 days. It answers what no weather model can: not how much rain falls upstream, but how much water arrives *here* — a different number, separated from the rain by a catchment, a soil moisture state and a day or two of travel time. ``` { "disclaimer": "Information only. Only national and regional authorities are authorised to issue flood warnings.", "daily": { "time": ["2026-08-01T00:00:00+00:00"], "river_discharge": [4.953], "soil_wetness_index": [0.448] } } ``` | Field | Meaning | | --- | --- | | `river_discharge` | Mean flow in m³/s over the 24 hours ending at that time. **Zero means the model has no river in that cell**, not that a river ran dry. | | `soil_wetness_index` | Catchment saturation, 0 dry to 1 saturated. High values mean further rain runs off rather than soaking in. | > **Important:** **This is not a flood warning and must not be presented as one.** The Copernicus licence reserves flood warnings to national and regional authorities within their area of responsibility, and that is the right arrangement — the people authorised to warn are the ones who can also close a road and evacuate a village. > > Discharge in m³/s, yes. An alert, a severity colour or an instruction to act, no, whatever the numbers look like on the day. The disclaimer travels in every response body, not only here, and it is there so that it reaches your users even if this page did not. --- # Degree Days API Heating and cooling degree days from ERA5-Land hourly reanalysis, back to 1950, anywhere on land — including the places with no weather station within 60 km. One scope — `dju` — covers this whole page. Web version: https://developers.climatememory.com/reference/degree-days ## Degree days ### Degree days - `GET https://api.climatememory.com/v1/degree-days` — scope `dju` — 2 credits, +1 per complete 365.25-day period in the span Heating and cooling degree days for a coordinate. | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `lat` | float | — | Required. | | `lon` | float | — | Required. | | `base` | number | preset | csv | 18 | Any base temperature in °C, or a preset: `uk` (15.5), `ashrae` (18.333), `iso`, `france`, `eurostat`. Up to 60 comma-separated, at no extra cost — see below. | | `method` | string | hourly | `hourly`, `costic`, `mean` or `eurostat`. Your contract decides this, not us. | | `start` | ISO date | 1 Jan of the end year | Maximum 10 years per request. | | `end` | ISO date | today | | | `type` | string | both | `HDD`, `CDD` or `both`. | | `breakdown` | string | monthly | `daily`, `weekly`, `monthly` or `yearly`. | | `elevation` | float (m) | — | True ground height of your site. Shifts the series from the cell's height to yours — see below. | | `format` | string | json | `json` or `csv`. CSV is a paid-plan feature. | ``` { "location": { "latitude": 36.75, "longitude": 3.06, "grid_cell": { "latitude": 36.7, "longitude": 3.1, "distance_km": 6.61, "resolution_km": 9 } }, "parameters": { "base_temperature_c": 18.0, "method": "hourly", "start": "2025-07-01", "end": "2026-06-30" }, "totals": { "hdd": 781.86, "cdd": 1331.61 }, "quality": { "days": 365, "coverage": 1.0, "missing_hours": 0, "provisional_days": 0 }, "breakdown": [ { "year": 2025, "month": 7, "hdd": 0.0, "cdd": 289.4, "days": 31 } ] } ``` > **Important:** **The `quality` block is not decoration.** `coverage` below 1.0 means hours were missing from the archive. `provisional_days` counts days filled from forecast rather than final reanalysis, because ERA5-Land publishes with about five days of latency. > > If you are settling a contract on these numbers, check both. A total computed over 0.98 coverage is not wrong, but it is not the same claim as one over 1.0 — and the difference is invisible in `totals`. ##### What a degree day is, in one paragraph A heating degree day measures how far below a base temperature the outside air sat, and for how long. At base 18 °C, an hour at 16 °C contributes `(18 − 16) / 24 = 0.083` HDD. Sum the hours and you have a number proportional to the energy a building needed. Cooling degree days are the mirror: how far *above* the base. It is the standard way to compare one heating season with another after the weather has been taken out of the comparison. ### Many bases, one request, one price - `GET https://api.climatememory.com/v1/degree-days?base=15,15.5,18,18.5,20` — scope `dju` — same as one base Up to 60 base temperatures in a single call, at the price of one. Reading and decoding the hourly series is the entire cost of a degree-days answer. Once that array is in memory another base is a subtraction over it, so asking for sixty costs what asking for one costs. The response gains a `by_base` array carrying every base in the order you listed them. `totals` and `breakdown` keep describing the *first* one, so code written before this existed still works unchanged. ``` "by_base": [ {"base": 15.0, "hdd": 402.11, "cdd": 1731.4}, {"base": 15.5, "hdd": 431.02, "cdd": 1706.3}, {"base": 18.0, "hdd": 781.86, "cdd": 1331.6} ] ``` Useful when you do not yet know which base reproduces a contract's figures, or when the same building is settled at different bases by different parties — a landlord on 15.5 and an energy supplier on 18 are both right, and this returns both in one call. ### Calculation methods - `GET https://api.climatememory.com/v1/degree-days/compare-methods` — scope `dju` — 2 credits, +1 per complete 365.25-day period in the span All four methods over the same period, side by side. - `GET https://api.climatememory.com/v1/methods` — no key needed — 0 credits The method definitions and their presets. No key needed. The method is a parameter because **your contract decides it, not us**. The same data, same base, same year, at Algiers: | Method | HDD 18 °C | CDD 18 °C | When to use it | | --- | --- | --- | --- | | `hourly` | 781.9 | 1331.6 | Default. Integrates the hourly deficit — the most physically faithful. | | `costic` | 741.7 | 1349.5 | **French DJU unifiés.** Required by French energy-performance contracts. | | `mean` | 659.9 | 1267.8 | Daily mean against base. The most common international convention. | | `eurostat` | 587.7 | 656.6 | European statistics. Bases are fixed by the definition and ignore `base`. | > **Important:** **Hourly and daily-mean differ by 18 % on the same data.** That is not a rounding difference — it is the difference between winning and losing an argument about an energy bill. Pick the method that reproduces the figures in your contract *before* you commit to a plan; that is what `compare-methods` is for, and it costs one request. `eurostat` ignores `base` entirely: the definition fixes its own thresholds, and honouring your parameter would produce a number that is not a Eurostat degree day while claiming to be one. `/v1/methods` needs no key. Use it to populate a method picker in your UI without spending anything, and without hard-coding a list that will go stale. ### Degree days by city - `GET https://api.climatememory.com/v1/degree-days/city/{country}/{slug}` — scope `dju` — 2 credits, +1 per complete 365.25-day period in the span The same, with the city's time zone and its urban heat island correction. Applies two corrections a bare coordinate cannot: the city's time zone, so days are cut locally, and its calibrated **urban heat island offset**. Reanalysis under-reads built-up areas by 1–3 °C. That biases cooling degree days low — material if you are sizing air conditioning, and invisible if you do not know to look for it. > **Note:** **`elevation` is not applied on this path**, and the omission is deliberate. The city offset is already computed against stations normalised to the city's own elevation, so a second lapse correction would count the same altitude twice — in the same direction, and plausibly enough that nobody would notice. If you need a specific building's altitude, use the coordinate endpoint with `elevation` and forgo the heat-island correction. ### Degree days for your building, not for the grid cell - `GET https://api.climatememory.com/v1/degree-days?elevation={metres}` — scope `dju` — same Lapse-rate correction from the cell's ground height to yours. A weather station is at the altitude it is at, and nobody can move it 600 m up the valley side for you. Our source is a model, so the cell's ground height is a number in the archive and the difference is arithmetic: 0.65 °C per 100 m. Over a heating season that is not a rounding error. Opt-in, and the response says exactly what it did: ``` "elevation": { "applied": true, "model_elevation_m": 42.0, "location_elevation_m": 1200, "temperature_offset_c": -7.53, "caveat": "Lapse-rate correction. It assumes temperature falls smoothly with height, which a valley floor under a winter inversion does not." } ``` The caveat travels in the response rather than only on this page, because the person reading the JSON six months from now is not the person who read the documentation. ### Pinning a cell, so a baseline stays comparable - `GET https://api.climatememory.com/v1/cells/resolve` — scope `dju` — 1 credit Which cell would answer for a point, and how far away it is. - `GET https://api.climatememory.com/v1/degree-days/cell/{cell_id}` — scope `dju` — 2 credits, +1 per complete 365.25-day period in the span Degree days for a named cell — no nearest-cell search, ever. Every other location form re-runs the nearest-cell search on each request, so the answer depends on what the archive holds *today*. That is the right default for a one-off lookup, and the wrong one for a baseline: a multi-year comparison is only a comparison if each year comes from the same place. As the archive widens, the nearest cell to a given site changes — an improvement in coverage that would otherwise arrive in your data as an unexplained step. ``` # once, at setup GET /v1/cells/resolve?lat=45.19&lon=5.72 → { "id": "era5l_45.20_5.70", "distance_km": 1.4, "resolution_km": 9 } # every time after that GET /v1/degree-days/cell/era5l_45.20_5.70?base=18&start=2015-01-01&end=2025-12-31 ``` `distance_km` is `0` on a pinned request by construction: you named the cell, so nothing was substituted for it. A cell that no longer exists returns `404 cell_not_found` rather than quietly falling back to a neighbour — which would reintroduce the very substitution you pinned it to avoid. > **Note:** `/v1/cells/resolve` is also the cheap way to find out *before* you pay for data that the nearest cell is 60 km away. It decodes nothing and reads no time series, and is priced accordingly at one credit. ## Shaping the answer ### Breakdown periods - `GET https://api.climatememory.com/v1/degree-days?breakdown={daily|weekly|monthly|yearly}` — scope `dju` — same Aggregate the same request to the period your contract settles on. Every degree-days response carries a `breakdown` array aggregated to the period you ask for. Contracts settle on different ones, so all four are available on the same request at the same price. | Period | Each row carries | | --- | --- | | `daily` | date, HDD, CDD, min/max/mean temperature | | `weekly` | ISO year and week, the date it starts, totals | | `monthly` | year, month, totals — the default | | `yearly` | year, totals | > **Note:** **Weeks are ISO weeks**, so a week belongs to the year containing its Thursday. 1 January 2023 falls in week 52 of 2022, and we report it there — which is what your spreadsheet will do too, and disagreeing with the spreadsheet is how a reconciliation meeting starts. Every bucket also carries `days`, so a partial month at the edge of your range is visible rather than silently short. A February with `"days": 12` is a February you should not compare against a full one. ### Monthly totals for a year - `GET https://api.climatememory.com/v1/degree-days/monthly` — scope `dju` — 2 credits Twelve monthly totals for one calendar year, with no daily series. - `GET https://api.climatememory.com/v1/coverage` — no key needed — 0 credits What the archive holds: first and last day with data, and the axis it will grow into. No key needed. A shorthand for the common case: `?lat=&lon=&year=2025` and twelve rows come back. Same data as `/v1/degree-days?breakdown=monthly` over the same span; fewer parameters to get wrong. `/v1/coverage` reports the archive's first and last day and its resolution, needs no key, and is the right thing to check before you ask for a period near the present edge — ERA5-Land runs about five days behind, and `provisional_days` in your response is the consequence. ``` { "start": "1950-01-01", "end": "2026-07-29", // last day that HOLDS data "axis_end": "2026-12-31", // where this run will stop growing "hours": 671256, "axis_hours": 674976, "cells": 86274, "resolution_km": 9.0, "run": "world-1950-2026-p1000" } ``` > **Important:** **`end` and `axis_end` are different questions, and only the first is about data.** A run is written against the whole calendar it will eventually fill — the 1950-2026 archive allocates every hour to 31 December 2026 — and the top-up fills it as Copernicus publishes. > > Until 2026-08-03 this endpoint reported the axis as `end`, so it credited the archive with roughly five months of hours that were empty. `end` is what you can ask for today; a request entirely past it is a `404 outside_archive` rather than a well-formed answer totalling zero. ### CSV export - `GET https://api.climatememory.com/v1/degree-days?format=csv` — scope `dju` — 4× the JSON call The breakdown rows as a CSV file. Paid plans only. Add `format=csv` to any degree-days request. You get the `breakdown` rows as a CSV file, named after the location and dates so it is still identifiable six months later in a downloads folder. ``` year,month,hdd,cdd,days,t_mean,coverage,provisional 2024,1,205.03,2.88,31,11.48,1.0,false 2024,2,168.44,4.10,29,12.31,1.0,false ``` **Paid plans only**, and bounded: the same maximum span as a JSON request, aggregated rows only — never the hourly series — and it costs four credits for every one the equivalent JSON call costs. A free key gets `402 export_not_in_plan`. That is deliberate rather than grudging. The archive is what you are paying for, and an unbounded export is how a competitor acquires it in an afternoon. The bounds are what let the format exist at all. ## The archive itself ### Hourly history - `GET https://api.climatememory.com/v1/historical` — scope `dju` — (years + 1) × (variables ÷ 2), rounded down, min 1 The reanalysis archive served raw: hourly weather back to 1950. | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `lat` | float | — | Required. | | `lon` | float | — | Required. | | `start` | ISO date | — | The span of one request is capped — see [Plans](https://developers.climatememory.com/start#plans). | | `end` | ISO date | — | | | `variables` | csv | a sensible subset | Ask `/v1/historical/variables` what this archive holds. | | `hourly` | bool | true | Include the hour-by-hour series. | | `daily` | bool | `false` — `true` for cities | Include daily aggregates, cut on *local* calendar days. | The same archive the degree days are built from, served directly: hourly, back to 1950, on a 9 km grid, everywhere on land. Same host and same `dju` scope — if you can call degree days, you can call this. > **Important:** **Today this archive holds temperature and nothing else.** It was ingested for degree days, and degree days need one variable. This page promised "humidity, wind, precipitation and solar radiation" until 2026-08-03 and the archive has never held any of them. > > `/v1/historical/variables` is the answer that is always current — it reads the promoted run rather than this sentence, needs no key, and costs nothing. Call it before you build against a field. > **Note:** **What makes it worth paying for is consistency.** A weather station record carries every move, every instrument change and every gap in its history, so a thirty-year trend computed from one is partly a trend in the instrumentation. A reanalysis has none of that: the model is the same model for every year in the record. For daily values over a long period, or for normals and trends, use the [Climate API](https://developers.climatememory.com/reference/climate#climate) instead — it holds the daily fields pre-derived, has no 9 km/1950 boundary, and answers a whole climatology in one call. ### History by city - `GET https://api.climatememory.com/v1/historical/city/{country}/{slug}` — scope `dju` — (years + 1) × (variables ÷ 2), rounded down, min 1 Identical, with the city's time zone and heat-island correction. Two things a coordinate cannot carry: the city's time zone, so days are cut where the city actually experiences them, and its calibrated urban heat island correction. `daily` defaults to true here, because a city request is almost always a request about days. ### Available variables - `GET https://api.climatememory.com/v1/historical/variables` — no key needed — 0 credits What the archive currently holds, with units, and which fields are derived. No key needed. Lists what the archive holds *right now*, with units, and which fields are derived rather than stored. An archive ingested for degree days alone holds temperature only, and this endpoint says so plainly instead of returning columns of `null`. > **Note:** Some fields are computed rather than stored: humidity from dew point, wind speed and direction from the u and v components. Storing what takes microseconds to calculate would add a third to the archive for nothing — but a derived field only appears when its *sources* are in the run, which is why this endpoint, and not a list written on a page, is the truth about what you can ask for. On the archive promoted today the sources are absent, so the answer is `temperature_2m` alone. --- # Climate API Eighty-seven years of daily climate, everywhere on land, from one archive — 1 January 1940 to within a week of today, on the ERA5 grid at 0.25° (about 28 km). Answers *is this normal?*, with the evidence. Web version: https://developers.climatememory.com/reference/climate ## The archive ### Daily climate - `GET https://api.climatememory.com/v1/climate/daily` — scope `climate` — 2 credits, +1 per complete 365.25-day period in the span Thirteen daily fields for any land coordinate, 1940 to last week. | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `lat` | float | — | Required. Any coordinate on land; the archive is global. | | `lon` | float | — | Required. | | `start` | ISO date | 1 Jan of the end year | Span is capped per request — see [Plans](https://developers.climatememory.com/start#plans). | | `end` | ISO date | the archive's last day | | | `fields` | csv | temperature and precipitation | Ask `/v1/climate/fields` what this run can answer for. | | `series` | bool | true | Include the day-by-day values. Set false when you only want the aggregates. | | `format` | string | json | `json` or `csv`. CSV is a paid-plan feature and returns the monthly rows. | This is **not** the [hourly history](https://developers.climatememory.com/reference/degree-days#historical) with a `GROUP BY` in front of it. The daily fields are derived once, at ingestion, on a day boundary chosen for the physics rather than for convenience, and the derived fields — apparent temperature, sunshine duration, mean humidity — are computed from the full hourly series rather than from the daily extremes. > **Important:** **Days are cut at solar midnight**, not at the political time zone. The offset is `round(longitude / 15)`. > > This is a deliberate choice and it is measurable: aggregating on UTC instead biases the daily minimum by 2.1 °C at Alice Springs, because the minimum falls just before dawn. A political time zone is worse still — two neighbouring cells either side of a border would have their days cut at different moments, and a map of daily maxima would show the outline of the time zones. > **Note:** **Values are the grid cell's, not the point's.** We do not adjust temperature to your exact elevation. Providers that do will therefore differ from us by a few tenths of a degree at the same coordinate — up to 0.4 °C in our own measurements — and neither figure is wrong. Ours is what the reanalysis says for that cell; theirs is that value plus a lapse rate applied to an elevation difference. We publish the cell so you can tell which you are getting: see [coverage and cells](https://developers.climatememory.com/reference/climate#climate-cells). ### Monthly aggregates - `GET https://api.climatememory.com/v1/climate/monthly` — scope `climate` — 2 credits, +1 per complete 365.25-day period in the span The same fields aggregated by calendar month. Sums for the accumulations, means and extremes for the rest. Same price as the daily series, because it reads the same column. Ask for it when a monthly figure is what you actually plot, rather than fetching thirty times the data and reducing it yourself — the transfer is the slow part, not the arithmetic. ### The climate of a place, in one call - `GET https://api.climatememory.com/v1/climate/summary` — scope `climate` — 3 credits, +1 per complete 365.25-day period in the span Monthly normals, annual series, records, trends and Köppen — no span limit. | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `lat` | float | — | Required. | | `lon` | float | — | Required. | | `start` | ISO date | the archive's first day | Asking for the whole record is the normal way to use this. | | `end` | ISO date | the archive's last day | | | `daily` | bool | false | Add `day_normals`: the normal of each calendar day of the year over your window, with the number of observations behind each one. | Everything a climate page states about a location, already reduced: twelve monthly normals, one row per complete calendar year, the all-time records with the dates they fell on, the least-squares warming trend with its significance, and the Köppen-Geiger code. For any land coordinate on Earth. > **Important:** **It is priced by span, and the default span is the whole archive.** Priced at **3 credits, +1 per complete 365.25-day period in the span** — so a call with no `start` covers 1940 to today and costs **89 credits**, not one. On the free plan that is roughly 110 calls a month. > > That is still the cheap way to get this answer: assembling it yourself would take nine capped [/v1/climate/daily](https://developers.climatememory.com/reference/climate#climate) calls over the same span, which cost more between them and return thirty-one thousand rows you then have to reduce. But it is not a lookup, and a page that calls it per visitor will empty a quota. Cache it — the answer for a coordinate changes once a day at most. > > Pass `start` when you do not need the whole record: thirty years costs 32 rather than 89. > **Note:** **No span limit here**, unlike [/v1/climate/daily](https://developers.climatememory.com/reference/climate#climate). That limit exists because a caller who can pull the day-by-day series can rebuild the archive; this endpoint returns no series at all. The full record comes back as about 62 kB — 100 kB with `daily=true` — against the thirty-one thousand daily rows it was reduced from, so it reconstructs nothing. One request replaces the nine capped daily calls the same answer would otherwise take. ##### `daily=true` is not the same as `normals?daily=true` And the difference is the point. [The normals endpoint](https://developers.climatememory.com/reference/climate#climate-normals) averages over a thirty-year WMO window; this one averages over whatever window you asked for — the whole archive by default. It is the only way to say *"9.4 °C above normal for a 30 July"* with eighty-seven years behind the claim rather than thirty. Each day carries its sample count, so widening to a fifteen-day centred normal is `Σ(mean·samples) / Σ(samples)` — exact, and without a second request. ##### Two conventions to know before you compare with another source A calendar year enters the annual series and the trend only if the archive holds **at least 360 of its days**, so the year in progress is excluded. A half-year reads as a collapse in rainfall and drags a trend line with it. `trends` is `null` below ten complete years. Under that, a slope is weather noise wearing a climate signal's clothes, and we would rather publish nothing than a confident wrong number. Each trend carries its own `p_value` and `significant` flag for the same reason — read them before you quote the slope. ## Normals ### WMO normals - `GET https://api.climatememory.com/v1/climate/normals` — scope `normals` — 3 credits A thirty-year mean over a WMO reference period. | Parameter | Type | Default | Description | | --- | --- | --- | --- | | `lat` | float | — | Required. | | `lon` | float | — | Required. | | `period` | string | 1991-2020 | `1991-2020` or `1961-1990`. Both are WMO reference periods. | | `fields` | csv | temperature and precipitation | | | `daily` | bool | false | Include the 366 per-day normals with their spreads, and the record high and low for each calendar day. | A normal is not an average of whatever period you happened to ask for. It is a thirty-year mean over a window the World Meteorological Organization fixes, so that two people quoting a normal are quoting the same thing. > **Note:** **This needs the `normals` scope**, which [the daily archive](https://developers.climatememory.com/reference/climate#climate) does not. A key that reads `/v1/climate/daily` happily can still get `403 scope_denied` here — see [Plans](https://developers.climatememory.com/start#plans) for which tier includes it. If what you want is "the normal over the whole record" rather than over a WMO window, use [/v1/climate/summary](https://developers.climatememory.com/reference/climate#climate-summary) instead — it needs only the `climate` scope and gives you eighty-seven years instead of thirty. ### Comparing two normals - `GET https://api.climatememory.com/v1/climate/normals/compare` — scope `normals` — 6 credits Both WMO periods and the change between them, in one call. This is the question most people are really asking when they ask for a normal — not *"what is normal here"* but *"how much has normal moved"*. Answering it in one request means the two halves cannot come from different runs, which is the failure mode of computing it yourself from two calls: the archive advances between them, and the difference you publish then contains a version change as well as a climate change. ## Metadata ### Coverage, fields and cells - `GET https://api.climatememory.com/v1/climate/coverage` — no key needed — 1 credit The run being served and the dates it spans. No key needed. - `GET https://api.climatememory.com/v1/climate/fields` — no key needed — 1 credit What this run can answer for, with units, and which fields are derived. No key needed. - `GET https://api.climatememory.com/v1/climate/cells/resolve` — scope `climate` — 1 credit Which cell answers for a point, and how far it is. - `GET https://api.climatememory.com/v1/climate/licensing` — no key needed — 0 credits The licence terms for this archive. No key needed. `cells/resolve` answers the question every gridded product should be asked before it is trusted: *which cell am I actually reading, and how far is it from the point I asked about?* At 28 km that distance can be 20 km, and knowing it is the difference between quoting a number and quoting a number responsibly. `fields` is the same contract as [/v1/historical/variables](https://developers.climatememory.com/reference/degree-days#historical-vars): it lists what the run holds rather than what the product might one day hold, so a client built against it does not break when the archive grows. > **Note:** **Pin the cell rather than the coordinate when a baseline has to stay comparable over years.** A coordinate is stable, but the cell that serves it would move if the grid ever changed — and a baseline that moves silently is the failure mode this endpoint exists to prevent. The degree-days API has the same pattern, with a dedicated path: see [pinning a cell](https://developers.climatememory.com/reference/degree-days#dd-pinned).