Quickstart
Get a free key (no card), then ask for a point. This asks for Amsterdam:
curl "https://nimboweather.com/v1/nowcast?lat=52.37&lon=4.90" \
-H "X-API-Key: $NIMBO_KEY"
// Node 18+ or a server-side function: keep the key out of web pages
const res = await fetch("https://nimboweather.com/v1/nowcast?lat=52.37&lon=4.90", {
headers: { "X-API-Key": process.env.NIMBO_KEY },
});
const data = await res.json();
if (!res.ok) throw new Error(`${data.error}: ${data.message}`);
console.log(data.summary?.text ?? "No radar coverage at this point");
for (const m of data.minutely.slice(0, 6)) console.log(m.time, m.precip_mm_h);
import os
import requests
r = requests.get(
"https://nimboweather.com/v1/nowcast",
params={"lat": 52.37, "lon": 4.90},
headers={"X-API-Key": os.environ["NIMBO_KEY"]},
timeout=10,
)
data = r.json()
if not r.ok:
raise RuntimeError(f"{data['error']}: {data['message']}")
print(data["summary"]["text"] if data["summary"] else "No radar coverage at this point")
for m in data["minutely"][:6]:
print(m["time"], m["precip_mm_h"])
The answer starts with a sentence you can show as it is, such as Light rain starting in 25 min, for about 40 min, then gives every step of the next hours.
Your key
Every call needs a key, except /v1/status (service health) and /v1/palettes (the colour tables of the tiles). Keys start with nimbo_. Send yours in one of three ways; they are equivalent:
- the header
X-API-Key: nimbo_...(recommended); - the header
Authorization: Bearer nimbo_...; - the query parameter
?key=nimbo_..., which map tiles require.
Keep the key on your server when you can. A key used in a web page or an app can be copied; if that happens, regenerate it from your dashboard. The old key stops working at once and your plan and usage are kept.
Request
GET /v1/nowcast
| Parameter | Required | Meaning |
|---|---|---|
lat | yes | Latitude in degrees, WGS 84. |
lon | yes | Longitude in degrees, WGS 84. |
horizon | no | How far ahead minutely goes, in minutes: 5 to 180. Default 180. |
step | no | Minutes between entries of minutely: 1, 5, 10 or 15. Default 5. It changes only the list: summary is the same for every step (see steps). |
Every call counts as one call against your monthly allowance, whatever the horizon and step.
Response
A JSON object. The fields you will use most:
{
"issue_time": "2026-11-12T17:10:00Z",
"data_age_s": 420,
"stale": false,
"coverage": "radar",
"radar_source": "opera",
"lat": 52.37, "lon": 4.9,
"summary": {
"text": "Light rain starting in 23 min, for about 40 min",
"rain_now": false,
"rain_starts_at": "2026-11-12T17:40:00Z",
"rain_ends_at": "2026-11-12T18:20:00Z",
"minutes_until_rain": 23,
"max_intensity": "light",
"now_source": "radar"
},
"observed_now": {"time": "2026-11-12T17:15:00Z", "precip_mm_h": 0.0},
"minutely": [
{"time": "2026-11-12T17:15:00Z", "precip_mm_h": 0.0, "precip_probability": 0.05, "precip_type": "rain"},
...
],
"attribution": ["..."]
}
This answer was made at 17:17 UTC: the forecast starts from the radar of 17:10, so its first entry (17:15) is already 2 minutes old, and minutes_until_rain (23) counts from 17:17.
| Field | Meaning |
|---|---|
issue_time | When the forecast was made (UTC): the time of the last radar image it starts from. |
data_age_s | Seconds since issue_time. A new forecast is issued every 10 minutes. |
stale | true when the radar data is late (see data age). |
notice | Present during a radar outage: a sentence saying since when the radar feed is down. |
coverage | radar inside the radar coverage, no_radar outside it (then summary is null and rates are null). |
radar_source | Which radar covers the point: opera almost everywhere, aemet where Spain’s national radars fill a gap. |
summary | The next three hours in a sentence and a few fields: see the summary. |
observed_now | The newest radar at the point (time, precip_mm_h), as the map shows it, when it is at most 15 minutes old; else null. The summary’s “now” is this observation when there is one. |
minutely[].time | Valid time of the entry (UTC): issue_time plus one step, two steps, ... The first entries can be in the past (an answer is typically 7 to 17 minutes younger than its issue time). |
minutely[].precip_mm_h | Rain rate in mm/h, rounded to 2 decimals: the value the map draws at this point. null where it is unknown (outside the radar coverage). |
minutely[].precip_probability | Chance of rain (0.2 mm/h or more), 0 to 1. |
minutely[].precip_probability_1mm_h, minutely[].precip_probability_5mm_h | Chance of at least 1 mm/h and of at least 5 mm/h, 0 to 1. Never above the chance of a lower threshold. |
minutely[].precip_type | Always rain for now: snow and other types are not told apart yet, and the value will stay rain until they are. |
horizon_skill | How well the forecast has scored at each lead time (hit score at 1 mm/h), so you can see where skill fades. |
attribution | The credit line for the data, to show with it (see attribution). |
The answer may carry more fields than these. Ignore the ones you do not use: new fields can be added at any time (see versions).
The summary
Rain means a rate of 0.2 mm/h or more, as published in minutely (rounded to 2 decimals, so an entry showing 0.2 is rain). The text speaks from the time of your request: “now” is the newest radar at the point when it is at most 15 minutes old (now_source: "radar", the same radar the map shows), else the forecast step nearest to your request (now_source: "forecast"). The reach it names (“in the next 3 hours”) is the length of the forecast, counted from its issue time: a request made a few minutes later has that many minutes less ahead of it, as the times in minutely show.
The text starts with the intensity at the start of the shower (the radar when it is raining now, else the shower’s first 30 minutes) and adds its peak when heavier (“Light rain (up to moderate)”), or “(lighter later)” when a shower of an hour or more ends lighter than it starts. max_intensity is the peak of the first shower: light (from 0.2 mm/h), moderate (from 2.5), heavy (from 10) or violent (from 50). Start times beyond an hour are rounded to 5 minutes (“in about 2 h 15 min”). The text is in English.
| Case | What you get |
|---|---|
| Rain coming | rain_now: false, start and end times, minutes_until_rain; text like “Light rain starting in 25 min, for about 40 min”. |
| Raining now | rain_now: true, minutes_until_rain: 0; text like “Light rain, stopping in about 30 min”. |
| Rain lasting past the horizon | rain_ends_at: null; the text says “for at least the next 3 hours” (raining now) or “lasting at least 2 h 25 min” (rain coming). |
| Several showers | The first one is described; the text adds “more showers later”. |
| No rain | Start and end null; text “No rain expected in the next 3 hours”. Where the map draws drizzle (0.1 to 0.2 mm/h) now or later, it adds “, a few drops possible”. |
| Outside the radar coverage | summary: null and coverage: "no_radar". |
| Radar data late | stale: true, start and end null; text “Forecast delayed: radar data is N min old”. |
Steps
step only resamples minutely; the summary always reads the forecast every 5 minutes. With step=5 (the default) each entry is the forecast at its time. With step=1 the entries are interpolated linearly between the 5-minute values (the first 5 minutes repeat the first one: the forecast starts at issue time + 5 min). With step=10 or 15 each entry carries the largest value (rate and probabilities) of the 5-minute steps it covers, the minutes after the previous entry up to its own time, so a 15-minute entry never hides a 5-minute shower the text speaks of.
Data age and outages
When new radar data stops arriving, there is nothing new to forecast from, and the API says so instead of guessing:
- after 18 minutes without new radar data,
stalebecomestrue; - after 30 minutes, answers carry a
noticeand the status page shows the outage; - after 2 hours, the API answers
503withforecast_unavailablerather than serve an old forecast.
If rain matters to your users, show data_age_s or the notice.
Errors
Every error is JSON with a code you can test and a sentence you can log:
{"error": "out_of_coverage", "message": "This point is outside the forecast area."}
Some errors add fields, such as the quota on a 429. The ones to handle:
| Status | Code | What to do |
|---|---|---|
| 401 | api_key_required, invalid_api_key | Send a valid key (see your key). Tiles also answer key_in_query_only to a key sent in a header. |
| 404 | out_of_coverage | The point is outside the forecast area: do not retry. |
| 422 | invalid_parameter | A parameter is missing or out of range: fix the request. |
| 429 | monthly_quota_exceeded | The month’s allowance is used up: wait for the reset or upgrade. |
| 429 | tile_rate_limited | Too many tiles in one second: wait Retry-After seconds. Not counted. |
| 503 | forecast_unavailable | No forecast younger than 2 hours (a radar outage): retry in 10 minutes. |
Limits and headers
Every answer to a call made with a key carries the state of your monthly allowance:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Calls included in your plan this month. |
X-RateLimit-Remaining | Calls left this month. |
X-RateLimit-Reset | When the allowance resets: the Unix time (seconds) at which the month ends, UTC. |
GET /v1/keys/me answers your key’s plan, monthly allowance and calls used this month. It is not counted, and it still answers once your calls have stopped.
On the Free plan, calls stop at 20,000 with a 429 until the month ends: no surprise bills. Paid plans keep working 10 % past their allowance, then stop the same way. Map tiles also have a short burst limit; a 429 from it carries only Retry-After (see tiles).
Attribution
On the Free plan, show “Rain forecast: Nimbo Weather” with a link to this site wherever you show the data. On every plan, also show the attribution field of the answer as it comes: it credits the radar data, as the data’s licence requires.
Versions
v1 is stable. We may add fields, but we won’t rename or remove any without 6 months’ notice by email. Breaking changes go to /v2.
Questions or a field you need: [email protected].