watersearch

The open API

Every figure on these pages, as data a program can read. Free, read-only, no key.

What the data is

Johannesburg Water (JW) publishes a status board for its reservoirs and towers, usually twice a day, and notices about planned and unplanned work. We read every board and notice JW has put on its website since March 2025 and keep them as a day-by-day record. The API gives you that record: what JW reported, for which reservoir or tower, on which day, and which suburbs JW links to each.

It is what JW reported about a reservoir or tower. It is not a reading of anyone's tap, and a day with no JW board is shown as a gap (no_report), never as a normal day.

Licence

CC BY 4.0 for this compilation and its readings. The facts are what Johannesburg Water (JW) reported on its status boards and notices: credit Johannesburg Water as the source.

The licence comes with every answer: in the licence field and in the X-Licence header. The licence is provisional until the site is public.

One call

Ask for a suburb's history by its name as it appears in the address of its page:

GET /api/v1/suburbs/melville/history

The answer is one row per reservoir or tower that supplies the suburb, per day (576 rows for Melville). Each row has the worst level JW reported that day and the same thing as a sentence (wording). Below, the answer cut to four rows:

{
  "licence": "CC BY 4.0 for this compilation and its readings. The facts are what Johannesburg Water (JW) reported on its status boards and notices: credit Johannesburg Water as the source.",
  "release": "2026-09-29-318d9dca",
  "source_view": "api.suburb_history",
  "caveat": "One row per suburb, per supply JW links it to (api.suburb_supply), per SAST day: the worst level JW reported for that supply that day, bad when no_water or critical, and on_bypass. A suburb draws on all its supplies, so a day is read across them, not from one. A day with no board is a gap (level no_report): it stays in every denominator and is never a normal day. JW reported the supply, not the suburb's taps: a street may differ.",
  "suburb": "melville",
  "suburb_name": "Melville",
  "days": [
    {
      "day": "2026-09-25",
      "asset_id": "hursthill-2-reservoir",
      "asset_name": "Hursthill 2 Reservoir",
      "level": "warned",
      "bad": false,
      "on_bypass": true,
      "boards": 2,
      "wording": "JW reported a warning that poor pressure to no water may occur for this supply on 25 September 2026, on bypass."
    },
    {
      "day": "2026-09-27",
      "asset_id": "hursthill-2-reservoir",
      "asset_name": "Hursthill 2 Reservoir",
      "level": "no_report",
      "bad": false,
      "on_bypass": false,
      "boards": 0,
      "wording": "JW published no report for this supply on 27 September 2026."
    },
    {
      "day": "2026-09-28",
      "asset_id": "hursthill-2-reservoir",
      "asset_name": "Hursthill 2 Reservoir",
      "level": "warned",
      "bad": false,
      "on_bypass": true,
      "boards": 1,
      "wording": "JW reported a warning that poor pressure to no water may occur for this supply on 28 September 2026, on bypass."
    },
    {
      "day": "2026-09-29",
      "asset_id": "hursthill-2-reservoir",
      "asset_name": "Hursthill 2 Reservoir",
      "level": "warned",
      "bad": false,
      "on_bypass": true,
      "boards": 2,
      "wording": "JW reported a warning that poor pressure to no water may occur for this supply on 29 September 2026, on bypass."
    }
  ]
}

Try it: /api/v1/suburbs/melville/history. For the statuses exactly as JW printed them, with links to JW's own post and board image, ask for a supply's reports.

Every address

Words in braces are the part you fill in. Each comes with a working example.

GET /api/v1/suburbs
Every suburb with a page: its id (used in every other suburb address), its everyday name, and whether JW's pages link it to a supply (coverage). Example.
GET /api/v1/suburbs/{slug}
One suburb: the reservoirs and towers JW links it to, how JW links each (with JW's own source links), and for each supply the days per year JW reported it at no water or critically low, against the city's median supply. A suburb draws on all its supplies at once. Example.
GET /api/v1/suburbs/{slug}/history
A suburb's history, one row per supply per day: the worst level JW reported for the supply that day, in JW's terms and in a plain sentence (wording). A day with no JW board is a gap (level "no_report"): it is not a normal day. Optional 'from' and 'to' (YYYY-MM-DD) narrow the days. Example.
GET /api/v1/supplies
Every reservoir, tower, pump station and direct feed JW names, with the systems JW lists it under. Example.
GET /api/v1/supplies/{asset_id}
One supply: what JW's own page says about it (capacity, demand, storage), its latest JW report, how long it has been on the boards, the days per year and per month JW reported it at no water or critically low (with the same month a year earlier where comparable), how long its last outages lasted, its rank in the city, and the suburbs JW links to it. Example.
GET /api/v1/supplies/{asset_id}/history
A supply's history, one row per day: the worst level JW reported, a plain sentence (wording), bypass, and each status as JW printed it that day (time and text). A day with no JW board is a gap (level "no_report"), not a normal day. Optional 'from' and 'to' (YYYY-MM-DD) narrow the days. Example.
GET /api/v1/supplies/{asset_id}/reports
The last statuses JW printed for a supply, newest first (default 50, at most 500): the board time, the level, the text as JW printed it, and links to JW's own post and board image. Example.
GET /api/v1/supplies/{asset_id}/outages
Every outage JW's boards show for a supply, newest first: a run of reports at no water or critically low, with its start, end and length in hours. 'censored' means JW reported no end, so the length is a minimum. 'no_board_days' counts days with no board inside it (C41: up to 4 such days between two outage reports do not end it); JW reported nothing for those days. Example.
GET /api/v1/supplies/{asset_id}/notices
Each end date JW stated in a notice that concerns this supply, against the first board after it on which JW reported the supply back (supplying fairly or better, off bypass). 'delta_hours' is how late; 'censored' means JW has not reported it back since. Example.
GET /api/v1/systems
JW's supply systems as JW names them, with how many supplies each lists. Example.
GET /api/v1/systems/{slug}
One JW supply system: its supplies, and per year the share of board rows printed under it that announced a scheduled overnight closure. Example.
GET /api/v1/regions
JW's seven operational regions (A to G), with how many suburbs JW's notices place in each. Example.
GET /api/v1/regions/{region}
One JW region: the suburbs JW's notices place in it, and the supplies JW links to those suburbs. Example.
GET /api/v1/city/ranking/{year}
Every supply ranked within its kind (reservoir, tower) by the days in a year JW reported it at no water or critically low (rank 1 is the most), with the city's median supply. Keyed by kind, then supply id. Example.
GET /api/v1/maintenance
Rand Water's maintenance windows the record covers, by start day. Example.
GET /api/v1/maintenance/{date}
One maintenance window (by its start day, YYYY-MM-DD): how many supplies JW reported out around it, the median and slowest days until JW reported them back, and each supply's days. Supplies on bypass before the window cannot be counted as recovered and are listed apart. Example.
GET /api/v1/releases
Every dated release of the record, newest first. Cite a figure with its release id. Where a bulk download (CSV and JSONL, with a manifest of row counts and sha256 checksums) exists, 'bulk' links to it. Example.
GET /api/v1/search
Suburb names like the query, best first (at most 10), with a score from 0 to 1. Use the id in the suburb addresses. Example.
GET /api/v1/openapi.json
This API described in OpenAPI 3, for code generators. Example.

Fair use

No key and no limit on requests. For the whole record, download a bulk release instead of calling every address: one file per table, dated, with checksums.

Levels

JW's words are placed on one scale, best to worst: adequate, fair, pressure, low, no_water, critical. A day at no_water or critical is counted as a day out (bad). A scheduled overnight closure is counted apart, never as an outage. How this record is made.

For programmers

The same addresses described in OpenAPI 3, for code generators: /api/v1/openapi.json. Every answer is JSON and names the release its figures come from (release) and what the figures can and cannot say (caveat). Cite a figure with its release.