Skip to content

API v1 · OpenAPI 3.1

Danube water level API and open data

Free JSON API for Danube water levels: stations with LDC/HDC thresholds, daily series, GeoJSON and an OpenAPI 3.1 contract, with rate limits and examples.

Base URL
https://danubealert.arcm.ro/api/v1
Authentication
Authorization: Bearer <key>

Get a free API key →OpenAPI 3.1 contract

Overview

The public v1 API returns the hydrological data of the Danube basin as JSON: stations with LDC/HDC thresholds, readings and forecasts, official thresholds, critical navigation points, hydrological bulletins, GeoJSON and the state of the sources. The full contract is at /api/v1/openapi.json (OpenAPI 3.1).

Plans and limits

Anonymous (no header): 60 requests per minute, CORS * on GET. With an integrator key (Authorization: Bearer <key>): 600 requests per minute. Keys are created for free from your account. A malformed, unknown or revoked key gets 401 (WWW-Authenticate: Bearer error="invalid_token"), not anonymous access; a revoked key stops working immediately.

  • Anonymous

    60requests / minute

    No header, CORS * on GET — enough to try every route.

  • Free API key

    600requests / minute

    Authorization: Bearer <key>, created in your account, shown once.

  • Heavy routes

    10requests / minute

    Series and exports ( observations, export ), per key or IP.

Example

The latest daily level at a station, with curl and with TypeScript.

curl
curl -sS "https://danubealert.arcm.ro/api/v1/stations/budapest/observations"
TypeScript
// Today's level at Budapest
const r = await fetch('https://danubealert.arcm.ro/api/v1/stations/budapest/observations?interval=daily&from=2026-10-01', {
  headers: { Authorization: `Bearer ${process.env.DANUBEALERT_KEY}` },
})
const { data } = await r.json()
console.log(data.at(-1)) // { observed_at: "2026-10-21T00:00:00Z", level_cm: 468 }

Endpoints

Every route of the public API v1, read-only ( GET ), with its parameters and a ready-to-run request.

Examples per route

Every route below has a runnable curl, JavaScript and Python request, its rate limit and the `_attribution` block you must show next to the data.

`_attribution` block
{
  "source": "National hydrological services of the Danube basin (see /v1/meta/sources)",
  "url": "https://danubealert.com/en/about",
  "data_updated_at": "2026-10-21T00:00:00Z"
}
_attribution · /export/daily
{
  "source": "National hydrological services of the Danube basin (see /v1/meta/sources)",
  "url": "https://danubealert.com/en/about",
  "license": {
    "id": "CC-BY-4.0",
    "name": "Creative Commons Attribution 4.0 International",
    "url": "https://creativecommons.org/licenses/by/4.0/"
  },
  "data_updated_at": "2026-10-21T00:00:00Z"
}

Stations

  • GET/api/v1/alerts/confirmThe emailed confirmation link of an alert or a daily report: it turns nothing on, it opens the confirmation page

    The link in the confirmation email of an alert or of a daily report, sent while the account has not yet proven its address. **It turns nothing on** ( corporate link scanners open emails by themselves ): the browser is taken to the public `/{lang}/alerts/confirm` page, with a single button that performs `POST /alerts/confirm`. With `format=json` ( or `Accept: application/json` ) it returns only the state: 200 with `status` `pending` / `active`, 404 for an invalid token.

    Parameters

    • t*query
    • formatquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/alerts/confirm?t=t"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/alerts/confirm?t=t')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/alerts/confirm?t=t", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • POST/api/v1/alerts/confirmTurns on the alert or daily report from the confirmation page ( the only step that activates it )

    The only step that turns the subscription on ( `activ = true` ): the confirmation page button or a client, with the token in the query ( `t` ) or in the body ( `{ "t": "…" }` ). Only a subscription still pending turns on; one switched off in the meantime from the account stays off. This is also where the account’s address is proven. The browser is taken to the subscription’s public page ( “on” ); a client receives JSON ( `kind`: `alert` / `report` ).

    Parameters

    • tquery
  • GET/api/v1/basin-precipitationCumulative 24 / 72 h precipitation per sub-basin, linked to the downstream station

    Rain accumulated over the last completed 24 h and 72 h, per sub-basin: the calculation point is a station on the upper course, and the row is linked to the downstream station — the precursor signal of a level rise there. The hourly series comes from an open weather source ( CC BY 4.0, see `_attribution` ); `window_end` is the moment up to which the hours are accumulated ( exclusive ), and `hours_24h` / `hours_72h` say how many hours with a value entered each window. A sub-basin whose run failed keeps its last `window_end`.

    Parameters

    • stationquery
    • riverquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/basin-precipitation"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/basin-precipitation')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/basin-precipitation", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/countries/{code}/stationsStations of a country

    The same rows as `/stations`, filtered by country; each has the `quality` block ( reference source, latest reading, G3 state and gauge datum ).

    Parameters

    • code*path
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/countries/HU/stations"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/countries/HU/stations')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/countries/HU/stations", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/report/todayThe day in one object: basin state, what changes today, stations above / below thresholds, 24–72 h forecast

    The day’s report as a single object, for the `/{lang}/today` page ( UX-9 ). `basin` is exactly the “Today’s situation” aggregate ( the total and the per-country rows ), `changes` is the F4 block ( the 24 h variation, the Q74 wave, the D2 anomaly ), `thresholds` lists the stations with a fresh reading below LDC / above HDC ( with the threshold, the level and the difference ), and `forecast` gives the **24–72 h** forecast from G2 ( source, issue, the source’s horizon, the min–max band per horizon and `orientativ` ), only for stations with a significant forecast: it reaches a threshold or moves at least 25 cm against the current level. The list is capped ( 12 per basin, 3 per country ), with the total before the cap; days 4–5 ( +96 / +120 h ) are not part of the report.

    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/report/today"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/report/today')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/report/today", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/stationsList of stations

    The paginated list of stations, with the level, the forecast and the `quality` block on every row: the reference source, the time of the latest reading, the G3 state ( fresh / “stale reading” / no data ) and the gauge datum ( national / other / absolute / unknown ).

    Parameters

    • countryquery
    • riverquery
    • qquery
    • limitquery
    • next_cursorquery
    • sortquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/stations"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/stations')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/stations", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/stations/{slug}A station, its LDC/HDC thresholds and the day’s anomaly against the climatology

    The station page: the LDC/HDC thresholds, the coordinates, the freshness of the reference source ( Q88 ), the 365-day series coverage ( Q94 ), the `quality` block — reference source, latest reading, G3 state and gauge datum ( H4 ) —, the `anomaly` block — today’s level deviation from the day’s climatology ( D1 ), absent below 10 reference years — and, when the station has a forecast, the `forecast` block with the source, `horizon_hours` ( how far the forecast goes ) and the horizons with their min–max band ( G2, UX-11 §8.1 p. 6 ).

    Parameters

    • slug*path
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/stations/budapest"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/stations/budapest')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/stations/budapest", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/stations/{slug}/eventsHistorical floods and droughts of a station

    The major floods and droughts documented for the station, newest first ( the year, then the peak day ). Each row has the type, the year, the peak day when the source publishes it, the value on the station gauge with its unit, the source code, the institution and the public URL. `?tip=viitura|seceta` filters the type. The rows come from the editorial `evenimente-istorice` collection, where the source and the evidence of every row are mandatory. `_attribution.data_updated_at` is the newest verification date of the returned rows.

    Parameters

    • slug*path
    • tipquery
    • limitquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/stations/budapest/events"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/stations/budapest/events')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/stations/budapest/events", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/stations/{slug}/neighborsUpstream / downstream neighbours and confluences

    Parameters

    • slug*path
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/stations/budapest/neighbors"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/stations/budapest/neighbors')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/stations/budapest/neighbors", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v2/stationsStation list ( v2, cursor + fields + format )

    The same data as `/v1/stations`, with the v2 list contract: keyset cursor pagination, `fields=` and `format=json|csv|geojson`. The response is cacheable (`ETag`, `Last-Modified`, `Cache-Control`), and `If-None-Match` returns `304`.

    Parameters

    • countryquery
    • riverquery
    • qquery
    • limitquery
    • next_cursorquery
    • fieldsquery
    • formatquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v2/stations"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v2/stations')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v2/stations", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/wizardAlert wizard data: sectors, countries, rivers and search

    Danube sectors from source to mouth ( by country, with the kilometre range and the sector stations ), plus each country rivers ordered by station count. With `q`, search stations and rivers by name; diacritics and Cyrillic are tolerated; `limit` ( 1–50 ) caps each list. Cached like the rest of the catalogue.

    Parameters

    • qquery
    • limitquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/wizard"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/wizard')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/wizard", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

Measurements and forecasts

  • GET/api/v1/forecast/accuracyOfficial forecast accuracy against actual readings, by source and horizon ( +24 / +48 / +72 h )10/min

    Backtest of the rows in `forecasts` against the actual readings in `readings`, per source × horizon ( +24 / +48 / +72 h ): mean absolute error ( `mae_cm` ), signed bias ( `bias_cm`, positive = the forecast was too high ) and the share of readings inside the published min–max band ( `within_band_pct`, only on samples with a band ). The list has every source with a forecast in the window, including those without pairs yet ( `samples: 0`, `null` means ). Default window: 90 days after the target moment; each forecast is compared with the nearest reading within ±6 h, preferring the issuer’s series, otherwise the station’s reference source. With no reading in the window, the forecast does not enter the sample. Without `station` the aggregate covers the whole basin; with `station`, only the requested station — the same figures as `/stations/{slug}/forecast-accuracy`. Heavy endpoint: 10 requests/min.

    Parameters

    • stationquery
    • daysquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/forecast/accuracy"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/forecast/accuracy')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/forecast/accuracy", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Heavy route: 10 requests/minute, even with a key.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/measurementsWater temperature, discharge, air temperature

    Parameters

    • station*query
    • measurequery
    • fromquery
    • toquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/measurements?station=budapest"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/measurements?station=budapest')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/measurements?station=budapest", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/observationsCompare up to 6 stations in one response10/min

    The same rules as one station’s observations; `interval` defaults to `daily` and the rate limit is the heavy one ( 10/min ).

    Parameters

    • stations*query
    • fromquery
    • toquery
    • intervalquery
    • riverquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/observations?stations=budapest%2Cmohacs"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/observations?stations=budapest%2Cmohacs')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/observations?stations=budapest%2Cmohacs", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Heavy route: 10 requests/minute, even with a key.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/stations/{slug}/climatologyDay-of-year climatology from the whole record: mean, median and percentiles10/min

    The station’s reference series for every calendar day ( `MM-DD` ): from the whole history of the daily rollup, how many years contributed, the mean, the median, the 10 / 25 / 75 / 90 percentiles, the minimum, the maximum and the standard deviation. A day’s value is the first reading of the local day from the station’s effective reference source. It is the basis for “today is X cm below the day’s normal”; the deviation itself is in the station page’s `anomaly` block and in `GET /summary/today`.

    Parameters

    • slug*path
    • dayquery
    • limitquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/stations/budapest/climatology"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/stations/budapest/climatology')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/stations/budapest/climatology", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Heavy route: 10 requests/minute, even with a key.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/stations/{slug}/dischargeDischarge series ( m³/s )

    The discharge series from the `masuratori` table ( the `debit` measurement ), written by the national hydrological services. One response contains a single source ( `source` ): without `source=` the source of the newest discharge measurement is used, so series from different sources are not mixed. `interval=hourly|daily` aggregates by mean. Heavy endpoint: 10 requests/minute anonymously.

    Parameters

    • slug*path
    • sourcequery
    • fromquery
    • toquery
    • intervalquery
    • formatquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/stations/budapest/discharge"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/stations/budapest/discharge')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/stations/budapest/discharge", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/stations/{slug}/forecastStation forecast ( where available )

    The latest issue from a single source ( `source` ); series from different sources are not mixed. `horizon_hours` says how far the issue’s forecast goes ( from +48 h to +120 h, depending on the source ), and every horizon has the min–max band published by the source ( `level_min_cm` / `level_max_cm`, `null` where it gives none ). Days 4–5 ( +96 / +120 h ) carry `orientativ: true`: they appear in the data but do not trigger alerts — the engine only uses 24–72 h.

    Parameters

    • slug*path
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/stations/budapest/forecast"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/stations/budapest/forecast')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/stations/budapest/forecast", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/stations/{slug}/forecast-accuracyForecast accuracy for one station, by source and horizon: how good the forecast is here10/min

    Backtest of the forecasts issued for the station in the path against its actual readings, per source × horizon ( +24 / +48 / +72 h ): mean absolute error ( `mae_cm` ), signed bias ( `bias_cm`, positive = the forecast was too high ) and the share of readings inside the published min–max band ( `within_band_pct` ). The list has **every source with a forecast** for the station in the window; a source with no forecast–reading pair still appears, with `samples: 0` and `null` means. Default window: 90 days after the target moment; the reading is taken at ±6 h, preferring the issuer’s series, otherwise the station’s reference source. Same figures as `/forecast/accuracy?station=<slug>`. Heavy endpoint: 10 requests/min.

    Parameters

    • slug*path
    • daysquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/stations/budapest/forecast-accuracy"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/stations/budapest/forecast-accuracy')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/stations/budapest/forecast-accuracy", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Heavy route: 10 requests/minute, even with a key.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/stations/{slug}/iceIce phenomena from the national bulletins, normalized per station and day

    The winter phenomena published in the national bulletins, normalized per station and day. Today’s source is a daily national bulletin: column `L` ( index 0–8 ) becomes `code`, and `phenomena` holds the canonical English codes ( `ice_floe`, `ice_bridge`, `shore_ice`… ). A day with index 0 is a real observation ( `ice: false`, `code: 0`, `phenomena: []` ), not missing data. RO / BG / RS / HU do not publish the phenomenon per station in the daily bulletin ( see the source description ), so they do not appear here.

    Parameters

    • slug*path
    • limitquery
    • fromquery
    • toquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/stations/budapest/ice"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/stations/budapest/ice')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/stations/budapest/ice", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/stations/{slug}/observationsLevel series ( cm )10/min

    A heavy endpoint for long anonymous history: 10 requests/minute ( §3 ). By default the series is the station’s **reference source** ( the national source; `source` in the body ). The history is layered: `source=` asks for one source’s series, and `include_legacy=true` adds the `legacy` block with the legacy series ( a different gauge: `level_cm` as the source publishes it, `level_cm_aligned` brought onto the national gauge zero when the datum is known ). The series are never mixed in `data`.

    Parameters

    • slug*path
    • sourcequery
    • include_legacyquery
    • fromquery
    • toquery
    • intervalquery
    • formatquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/stations/budapest/observations"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/stations/budapest/observations')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/stations/budapest/observations", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Heavy route: 10 requests/minute, even with a key.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/stations/{slug}/percentileToday’s percentile against the same day over the last 30 years10/min

    The latest day’s level ( the first reading of the local day, from the reference source ) against the same calendar day over the last 30 years. `percentile` is 0…100, `category` is one of very_low / low / normal / high / very_high; below 10 reference years, `percentile` is null. One query over the year window, not one per day.

    Parameters

    • slug*path
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/stations/budapest/percentile"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/stations/budapest/percentile')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/stations/budapest/percentile", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Heavy route: 10 requests/minute, even with a key.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/stations/{slug}/recordsHistorical records10/min

    Parameters

    • slug*path
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/stations/budapest/records"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/stations/budapest/records')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/stations/budapest/records", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Heavy route: 10 requests/minute, even with a key.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/stations/{slug}/same-dayThe same day in past years10/min

    Parameters

    • slug*path
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/stations/budapest/same-day"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/stations/budapest/same-day')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/stations/budapest/same-day", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Heavy route: 10 requests/minute, even with a key.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/stations/{slug}/winterWinter phenomena and ice thickness

    The winter observations published for the station, newest first: the phenomenon text and/or the ice thickness in cm. With no observations, the list is empty.

    Parameters

    • slug*path
    • limitquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/stations/budapest/winter"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/stations/budapest/winter')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/stations/budapest/winter", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/summary/low-waterLow water: consecutive days below LDC, by sector, with history and comparison

    Hydrological drought on the Danube, by sector ( the river countries, from upstream to downstream ): for the chosen year, the longest uninterrupted period below LDC of a station in the sector, the record station, how many stations had low water, the station-days below LDC, the sector’s history and the comparison with the reference years 2003, 2015, 2018 and 2022. Same rules as the “Low water” tab on `/{lang}/statistics`: thresholds are judged on the reference reading from the rollup, and a missing day breaks the period.

    Parameters

    • yearquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/summary/low-water"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/summary/low-water')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/summary/low-water", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/summary/todayToday’s situation: levels, thresholds, forecasts, the day’s anomalies and what’s changing today across the basin

    The day’s aggregate, per country: how many stations are below LDC and above HDC ( with the share of stations that have thresholds ), how many have no reading in the last 48 h, the 24 h trend ( rising / falling / steady ) and the forecast for the next 24–72 h. D2 adds the deviation from the day’s normal ( the D1 climatology ): how many stations are above p75, below p25 or within the normal range, and the ranking of the largest standardized deviations. F4 adds “what changes today”: the first significant changes from the 24 h variation, from the Q74 wave ( upstream → downstream travel time ) and from the D2 anomaly, per basin ( `changes` ) and per country ( `changes_by_country` ). Same rules as the `/{lang}/today` page: thresholds are judged on the fresh reference reading, and the forecast comes from a single issue.

    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/summary/today"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/summary/today')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/summary/today", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

Rivers and countries

  • GET/api/v1/countriesCountries in the basin
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/countries"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/countries')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/countries", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/riversRivers in the basin and their station counts
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/rivers"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/rivers')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/rivers", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/rivers/{rau}River graph: mouth, tributaries, countries

    Parameters

    • rau*path
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/rivers/sava"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/rivers/sava')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/rivers/sava", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v2/countriesBasin countries, cursor-paginated ( v2 )

    The basin register countries, with names in the product’s languages; the cursor is the ISO code.

    Parameters

    • limitquery
    • next_cursorquery
    • fieldsquery
    • formatquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v2/countries"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v2/countries')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v2/countries", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v2/riversBasin rivers, cursor-paginated ( v2 )

    The rivers in the station register, with the number of stations per country. The cursor is the normalized river code; `fields=` and `format=json|csv|geojson` apply the same way on every v2 list.

    Parameters

    • limitquery
    • next_cursorquery
    • fieldsquery
    • formatquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v2/rivers"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v2/rivers')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v2/rivers", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

Exports

  • GET/api/v1/export/country/{cod}CSV / JSON export by country10/min

    Parameters

    • cod*path
    • formatquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/export/country/HU"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/export/country/HU')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/export/country/HU", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Heavy route: 10 requests/minute, even with a key.

    The export carries `_attribution` too, with the license in `_attribution.license` (CC BY 4.0); show `_attribution.source`, `_attribution.url` and the license with the file.

  • GET/api/v1/export/dailyDaily CSV / JSON export by country10/min

    The previous day’s file for one basin country, with the CC BY 4.0 licence and attribution. The CSV puts them in the `Link`, `X-Attribution*` and `X-License*` headers; the JSON in `_attribution` ( including `license` ). The response is cacheable: `ETag`, `Last-Modified`, `Cache-Control`, and `If-None-Match` returns 304 with the same headers.

    Parameters

    • country*query
    • formatquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/export/daily?country=HU"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/export/daily?country=HU')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/export/daily?country=HU", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Heavy route: 10 requests/minute, even with a key.

    The export carries `_attribution` too, with the license in `_attribution.license` (CC BY 4.0); show `_attribution.source`, `_attribution.url` and the license with the file.

  • GET/api/v1/export/rangeLarge-interval export for researchers ( CSV / Parquet, cursor-paginated, with per-row licenses )10/min

    One page of the daily rollup (`readings_daily`) over the `from`–`to` interval, only from sources with an explicit redistribution licence ( the Q90 licence register ). Every row has the source’s `license` column; the full attribution and the terms URL are in the `/export/range/licenses` manifest and in the `X-License` / `X-Attribution` / `Link` headers. Pagination uses `cursor` ( `X-Next-Cursor`, `Link: rel="next"` ), tied to the filters by a fingerprint: two consecutive pages neither repeat nor skip rows. Without a key: at most 366 days and 500 rows per page; with a key (`Authorization: Bearer`): 20 years and 5 000 rows per page. The export is a heavy endpoint ( 10 requests/minute ).

    Parameters

    • from*query
    • to*query
    • countryquery
    • formatquery
    • limitquery
    • cursorquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/export/range?from=2026-01-01&to=2026-10-23"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/export/range?from=2026-01-01&to=2026-10-23')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/export/range?from=2026-01-01&to=2026-10-23", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Heavy route: 10 requests/minute, even with a key.

    The export carries `_attribution` too, with the license in `_attribution.license` (CC BY 4.0); show `_attribution.source`, `_attribution.url` and the license with the file.

  • GET/api/v1/export/range/licensesSource licenses and attribution for the range export

    The manifest of the sources that can appear in `GET /export/range`: only those with an explicit redistribution licence, each with the institution, the licence, the terms URL, the attribution, the transformation applied ( daily aggregate ) and the verification date. A refused or unknown source does not appear in the manifest and is not exported.

    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/export/range/licenses"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/export/range/licenses')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/export/range/licenses", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The export carries `_attribution` too, with the license in `_attribution.license` (CC BY 4.0); show `_attribution.source`, `_attribution.url` and the license with the file.

  • GET/api/v1/export/river/{rau}CSV / JSON export by river10/min

    Parameters

    • rau*path
    • formatquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/export/river/sava"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/export/river/sava')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/export/river/sava", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Heavy route: 10 requests/minute, even with a key.

    The export carries `_attribution` too, with the license in `_attribution.license` (CC BY 4.0); show `_attribution.source`, `_attribution.url` and the license with the file.

Thresholds, navigation, bulletins

  • GET/api/v1/bulletinsHydrological bulletins by country

    Parameters

    • countryquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/bulletins"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/bulletins')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/bulletins", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/navigationCritical points and navigation restrictions

    Only the active rows; expired restrictions ( a past `data_end` ) are no longer returned, and restrictions come only from the official sources kept. The response is sorted by rkm.

    Parameters

    • riverquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/navigation"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/navigation')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/navigation", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/thresholdsOfficial thresholds ( LDC/HDC, RNW/HSW, flood defence )

    Only the active thresholds ( archived values stay in the platform with `activ: false` and are not published ). `station` is the station slug; an unknown slug returns 404.

    Parameters

    • stationquery
    • pagequery
    • limitquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/thresholds"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/thresholds')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/thresholds", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v2/bulletinsHydrological bulletins, cursor-paginated ( v2 )

    The national bulletins, newest first; `country` filters, the cursor is the bulletin id.

    Parameters

    • countryquery
    • limitquery
    • next_cursorquery
    • fieldsquery
    • formatquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v2/bulletins"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v2/bulletins')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v2/bulletins", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v2/navigationNavigation, one paginated list ( v2 )

    Critical points and active restrictions in a single list, each row with `kind` (`punct_critic` or `restrictie` ); the cursor composes `kind:id`. Geometry exists on points.

    Parameters

    • riverquery
    • limitquery
    • next_cursorquery
    • fieldsquery
    • formatquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v2/navigation"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v2/navigation')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v2/navigation", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v2/thresholdsOfficial thresholds, cursor-paginated ( v2 )

    The active thresholds, with `station` ( slug ) as a filter; the cursor is the threshold id. An unknown slug returns 404, as in v1.

    Parameters

    • stationquery
    • limitquery
    • next_cursorquery
    • fieldsquery
    • formatquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v2/thresholds"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v2/thresholds')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v2/thresholds", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

Alert episodes

  • GET/api/v1/episodesPublic LDC/HDC episodes

    Parameters

    • countryquery
    • typequery
    • limitquery
    • next_cursorquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/episodes"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/episodes')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/episodes", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/episodes/{id}One alert episode

    Parameters

    • id*path
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/episodes/1"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/episodes/1')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/episodes/1", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

GeoJSON

  • GET/api/v1/geojson/riversNavigable Danube line + Sava / Tisza
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/geojson/rivers"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/geojson/rivers')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/geojson/rivers", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/geojson/stationsFeatureCollection of the stations

    Parameters

    • countryquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/geojson/stations"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/geojson/stations')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/geojson/stations", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

Sources and data quality

  • GET/api/v1/meta/availabilityMonthly availability per source ( 12 months ): days with data and the recent runs

    The public “data transparency” report: one month per source over the last 12 calendar months, with the days counted (`days`, the current month only up to yesterday), the days with at least one reading (`days_with_data`) and their percentage. `last_7_days` gives the runs and failures of the last 7 days from the Q62 journal (`runs` / `failures`; `null` = no journal). Figures only: no error messages, no URLs, no station details. Without the daily rollup ( an instance without ingest ) `sources` is empty, not “0 %”.

    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/meta/availability"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/meta/availability')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/meta/availability", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/meta/healthSource health
    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/meta/health"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/meta/health')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/meta/health", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/meta/qualityData quality per station: suspect readings and source state

    Aggregate per station: the number of suspect readings ( deviation above a threshold from the neighbours’ median ±6 h ), the latest reading and the state of the source that brought it ( `source_state`: `ok`, `eroare`, `stale` — “no readings within term” ).

    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/meta/quality"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/meta/quality')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/meta/quality", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v1/meta/sourcesLast update per crawl source

    One row per crawl source: the last successful run, its state, the SLA, **the last write**, **the rows read and the readings written in the last 24 h**, **7- and 30-day uptime**, **the average delay** of the data ( the mean age of the freshest reading, over 7 days ) and **the last error** ( kept even after the source recovers ). `stare` can be `cazuta` when the source has written nothing for more than 2 × its interval, and `eroare` means the last run failed **and** the data before it fell outside the source’s SLA; a network failure while the data is still within term leaves `stare` unchanged ( the error stays in `ultima_eroare` ). `uptime_7d` / `uptime_30d` are fractions 0–1; `null` = the window cannot be computed or `readings` is unavailable now, not “perfect source”.

    curl
    curl -sS "https://danubealert.arcm.ro/api/v1/meta/sources"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v1/meta/sources')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v1/meta/sources", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

  • GET/api/v2/meta/sourcesCrawl sources, cursor-paginated ( v2 )

    The last run and the quality over time per source; the cursor is the source code. The fields are those of `/v1/meta/sources`.

    Parameters

    • limitquery
    • next_cursorquery
    • fieldsquery
    • formatquery
    curl
    curl -sS "https://danubealert.arcm.ro/api/v2/meta/sources"
    JavaScript
    const r = await fetch('https://danubealert.arcm.ro/api/v2/meta/sources')
    if (!r.ok) throw new Error(`HTTP ${r.status}`)
    const body = await r.json()
    console.log(body)
    console.log(body._attribution.source, body._attribution.url)
    Python
    import requests
    
    r = requests.get("https://danubealert.arcm.ro/api/v2/meta/sources", timeout=30)
    r.raise_for_status()
    body = r.json()
    print(body)
    print(body["_attribution"]["source"], body["_attribution"]["url"])

    Rate limit: Anonymous: 60 requests/minute (sliding window); with `Authorization: Bearer <key>`: 600/minute.

    The response carries `_attribution`; show `_attribution.source` and `_attribution.url` next to the data.

Rate limiting

Sliding 60-second window; the observation series is a heavy endpoint (10 per minute). Above the limit: 429 with retry_after_seconds in the body and Retry-After plus X-RateLimit-* headers. Refused requests do not count against the next window.

Rate limiting
HeaderMeaning
X-RateLimit-Limit60 / 600 / 3000 / 10
X-RateLimit-Remaining0 … limit
Retry-Afters ( 429 )
429 · retry_after_secondsJSON

Typed TypeScript client

Types generated straight from the contract — every route, parameter and response, no hand-written models.

TypeScript
npx openapi-typescript https://danubealert.arcm.ro/api/v1/openapi.en.json -o danubealert.d.ts

import type { paths } from './danubealert'
type Observatii = paths['/stations/{slug}/observations']['get']['responses']['200']['content']['application/json']

Open-data export

The previous day's files, one row per station, for every country of the basin, as CSV or JSON. Responses are cacheable (ETag, Last-Modified, Cache-Control); the licence and attribution are in the file headers and in the _attribution block of the JSON.

Licence: Creative Commons Attribution 4.0 International · Attribution: DanubeAlert and the national hydrological services · The previous day, in the country's time zone.

Widget configurator

Show a station level on your own site: pick the station, theme, language and branding, see the widget live and copy the iframe snippet into any CMS.

Your widgets in three steps

Pick the type and the settings — the preview and the snippet update instantly.

Theme
Size

Preview ( thumbnail )

Snippet to paste

<iframe
  src="https://danubealert.arcm.ro/embed/v1/station?slug=achleiten&theme=auto&accent=00BABE"
  title="Danube water level — Achleiten"
  width="100%"
  height="300"
  style="border:0;max-width:640px;"
  loading="lazy"
  referrerpolicy="strict-origin-when-cross-origin"
></iframe>

Full contract: /api/v1/openapi.en.json · /api/v1/openapi.json · Data: the national hydrological services, via DanubeAlert