Docs
Getting the data
Every dataset is in the portal to explore and download, and in the API for your own systems and spreadsheets. Each record names its source, and each release says what changed.
Accounts are open by invitation during early access. Join early access to get one.
Your first call
- Sign in at app.unmappedlabs.com, switch on the datasets you need, and create a key under API keys.
- Ask for the latest exchange rates:
curl https://api.unmappedlabs.com/v1/datasets/reserve-bank-series/latest \
-H "Authorization: Bearer um_live_your_key"
The answer is JSON: the dataset, its release version, and the records, each with its source.
Keys
Send your key in the Authorization header: Bearer um_live_…. Keys read the datasets your account has switched on, and you can have up to ten, so use one per system and revoke any that leak. For CSV only, a key can go in the address instead (?key=), because spreadsheets can’t send headers; make a separate key for those links.
Without code
In the portal, open a dataset’s Explore and download: filter it, see it as a table, download a CSV for Excel, or make a spreadsheet link that refreshes itself. See spreadsheets below.
Records and paging
GET /v1/datasets/{id}/records returns records a page at a time: 100 as JSON, up to 1,000 as CSV (?limit= asks for fewer). When there are more, the answer has a next link (and a Link header); follow it until next is null.
{
"dataset": "dam-levels",
"version": "2026-10-26",
"count": 227,
"offset": 0,
"returned": 100,
"records": [ … ],
"next": "https://api.unmappedlabs.com/v1/datasets/dam-levels/records?cursor=…"
}
A next link stays on the release it started on, so a page-through never mixes two releases, even if new data arrives halfway. Time series come newest first.
Filters
Each dataset has its own filters (listed under datasets), and every one takes from and to dates. Combine them freely:
/v1/datasets/reserve-bank-series/records?currency=USD&from=2026-01-01
/v1/datasets/dam-levels/records?province=ZA-WC
/v1/datasets/electricity-tariffs/records?distributor=CPT&category=residential
An unknown parameter is an error that names the filters that work, so a typo never quietly returns everything.
Latest values
GET /v1/datasets/{id}/latest returns the newest record for each currency, rate, dam or tariff: often all a website, dashboard or quote needs. It takes the same filters.
CSV
Add .csv to records or latest (or ?format=csv). Nested values become columns: source.publisher, source.url. Where a record holds a list, such as a tariff’s charges, there’s one row per item, with the record’s own fields repeated. Files are UTF-8 with a byte-order mark, so Excel opens them correctly.
Spreadsheets and Power BI
Build the CSV address with your filters and a key, for example:
https://api.unmappedlabs.com/v1/datasets/reserve-bank-series/records.csv?currency=USD&from=2026-01-01&key=um_live_your_spreadsheet_key
- Google Sheets: in a cell,
=IMPORTDATA("the address"). It refreshes about every hour. - Excel: Data, then From Web, then paste the address. Refresh with Data, Refresh All. To keep the key out of the address, choose Advanced and add a header
AuthorizationwithBearer um_live_…, and leavekeyoff. - Power BI: Get data, then Web, then the same address (or Advanced with the header).
The portal’s explorer makes these links for you. Every refresh counts towards your daily records, so filter to what you need.
Releases and changes
Data arrives in releases. GET /v1/datasets/{id}/releases lists them, newest first, with how many records each added, changed and removed. Datasets kept current by our daily jobs (rates, exchange rates, dam levels) release when their source has something new; others release as they’re built and checked.
Our promise for v1: changes only add things. We don’t rename or remove a field, endpoint or filter without telling key holders by email at least 90 days before and listing it here. The API is also described for tools at /v1/openapi.json (OpenAPI 3.1), which Postman and code generators read.
Limits
- Records a day: each account can read a set number of records a day across all its keys and the portal’s downloads. The headers
X-Daily-Limit,X-Daily-RemainingandX-Daily-Resetsay where you stand; it resets at 02:00 South African time. - Requests a minute: up to 120 per key. Ask us if you need more.
- Use: the data is licensed for your own business and products, as the terms of use set out.
Errors
| Status | Error | What to do |
|---|---|---|
| 400 | bad_parameter, bad_limit, bad_cursor | Fix the parameter the message names. |
| 401 | unauthorized | Send a valid key. |
| 402 | payment_required | The dataset is paid; buy it in the portal. |
| 403 | not_enabled, terms_required | Switch the dataset on, or accept the terms, in the portal. |
| 409 | not_available | The dataset is still being built. |
| 429 | rate_limited, daily_limit | Slow down, or wait for the daily reset. |
Every error is JSON: {"error": "…", "message": "…"}.
Code examples
Python, every US dollar rate this year:
import requests
url = "https://api.unmappedlabs.com/v1/datasets/reserve-bank-series/records"
params = {"currency": "USD", "from": "2026-01-01"}
headers = {"Authorization": "Bearer um_live_your_key"}
rows = []
while url:
page = requests.get(url, params=params, headers=headers, timeout=30).json()
rows += page["records"]
url, params = page["next"], None # the next link carries the filters
JavaScript, the latest level of every Western Cape dam:
const res = await fetch(
"https://api.unmappedlabs.com/v1/datasets/dam-levels/latest?province=ZA-WC",
{ headers: { Authorization: "Bearer um_live_your_key" } }
);
const { records } = await res.json();
for (const d of records) console.log(d.dam, d.percent_full);
Datasets
The datasets with data so far. Every dataset on the plan is in the catalogue.
UM-001 Municipal electricity tariffs
One record per tariff, per distributor, per financial year. Updated annual. NERSA approves municipal tariffs from about April to June each year and they take effect on 1 July. Eskom's own tariffs take effect on 1 April. Coverage: Every tariff in NERSA’s FY2026/27 decisions for City of Cape Town (18 tariffs) and eThekwini (15 tariffs). Next: the other six metros (Johannesburg City Power, Tshwane, Ekurhuleni, Nelson Mandela Bay, Buffalo City, Mangaung Centlec), then the next 12 largest distributors, then the rest and Eskom.
/v1/datasets/electricity-tariffs/records · /latest gives the newest financial year of every tariff, for each distributor
| Filter | Example | Meaning |
|---|---|---|
distributor | CPT | Municipal Demarcation Board code of the distributor |
category | residential | Tariff category: residential, commercial, large_power, embedded_generation, wheeling, generator or public_lighting |
financial_year | 2026/27 | Municipal financial year |
from, to | 2026-01-01 | Dates, on effective_from |
Fields (13)
| Field | Type | Meaning |
|---|---|---|
id | string | Stable id: country, distributor, tariff and financial year |
distributor | string | Licensed distributor: Eskom or the municipality |
distributor_code | string | Municipal Demarcation Board code of the municipality (CPT, ETH) |
tariff | string | Tariff name as the decision gives it |
category | enum | residential, commercial, large_power, agricultural, embedded_generation, wheeling, generator, public_lighting or other |
financial_year | string | Municipal financial year, July to June |
effective_from | date | Date the tariff takes effect |
status | enum | Where the tariff is in NERSA's approval process |
charges | array | Each charge: name, season (high_demand is winter, usually June to August), time-of-use period, meter, kWh block, unit, the approved value, last year's approved value (previous) and, where the decision shows it, what was actually charged last year (previous_charged) |
vat | enum | Whether the charges include VAT, as the source states it |
source | object | Publisher, document, decision date, page and link |
flags | array | Machine-readable notes from checking: summary_rate_differs, suspect_value_in_source, obsolete_tariff, unit_not_stated |
notes | array | What each flag means for this record, in words |
curl "https://api.unmappedlabs.com/v1/datasets/electricity-tariffs/records?distributor=CPT" \
-H "Authorization: Bearer um_live_your_key"
UM-028 Dam levels with full history
One record per dam, per weekly State of Dams report. Kept current automatically. Weekly State of Dams: checked daily at 04:00 UTC, released when DWS publishes a new report (Mondays). Coverage: All 227 dams in the Department of Water and Sanitation's weekly State of Dams: 224 in South Africa, Katse and Mohale in Lesotho and Maguga in Eswatini, from the report of 26 October 2026, each with last week's and last year's level. History grows week by week; earlier years from DWS's verified daily data come next.
/v1/datasets/dam-levels/records · /latest gives the latest weekly reading of every dam
| Filter | Example | Meaning |
|---|---|---|
station | D3R002 | DWS station code of a dam |
province | ZA-WC | Province code, ISO 3166-2 |
country | ZA | ZA, or LS and SZ for the Lesotho and Eswatini dams |
river | Orange River | River, as DWS names it |
from, to | 2026-01-01 | Dates, on report_date |
Fields (14)
| Field | Type | Meaning |
|---|---|---|
id | string | Station code and report date (D3R002-2026-10-26) |
station | string | The Department of Water and Sanitation's station code for the dam; stable from week to week |
dam | string | Dam name as DWS gives it |
river | string | River the dam is on |
country | string | ZA, or LS and SZ for the Lesotho and Eswatini dams that supply South Africa |
provinces | array | Province codes, ISO 3166-2 (ZA-FS); two where a dam sits on a provincial border |
report_date | date | Date of the weekly report |
full_supply_capacity | number | Full supply capacity, in million cubic metres |
percent_full | number | Storage as a % of full supply capacity this week; above 100 when the dam is over full supply |
percent_full_last_week | number | The same, a week earlier |
percent_full_last_year | number | The same, a year earlier |
volume | number | Water stored, in million cubic metres: capacity × % full, worked out by us |
flags | array | not_this_week (DWS shows its latest available reading, not this week's) and above_full_supply |
source | object | Publisher, report and the DWS page the figures come from |
curl "https://api.unmappedlabs.com/v1/datasets/dam-levels/records?station=D3R002" \
-H "Authorization: Bearer um_live_your_key"
UM-142 Reference interest rates and legal limits
One record per rate, per date it took a new value. Kept current automatically. Reserve Bank series: checked daily at 04:00 UTC, released when a rate takes a new value (ZARONIA and SABOR do most business days). Coverage: The Reserve Bank's policy rate (repo rate) and prime lending rate with every change since 2001, and the ZARONIA (since August 2022) and SABOR (since August 2006) overnight rates, daily. Still to add: the legal limits that follow from the repo rate (the prescribed rate of interest and the National Credit Act maximum rates), once each rule is checked against the law's text.
/v1/datasets/reference-rates/records · /latest gives the current value of each rate
| Filter | Example | Meaning |
|---|---|---|
rate | prime_lending_rate | policy_rate (the repo rate), prime_lending_rate, zaronia or sabor |
from, to | 2026-01-01 | Dates, on effective_from |
Fields (10)
| Field | Type | Meaning |
|---|---|---|
id | string | Rate and effective date (policy-rate-2026-09-19) |
rate | enum | policy_rate (the repo rate), prime_lending_rate, zaronia or sabor |
name | string | The rate's full name |
effective_from | date | First day the rate had this value, as the Reserve Bank's series shows it |
value | number | The rate, % a year |
previous | number | The value before this one, % a year |
change | number | Change from the previous value, in percentage points |
series_code | string | The Reserve Bank's time series code (MMRD002A) |
flags | array | first_in_series, and spread_not_3_5 where prime isn't the policy rate plus 3.5 points |
source | object | Publisher, series and the API address it came from |
curl "https://api.unmappedlabs.com/v1/datasets/reference-rates/records?rate=prime_lending_rate" \
-H "Authorization: Bearer um_live_your_key"
UM-150 Reserve Bank statistical series
One record per currency per business day. Kept current automatically. Reserve Bank series: checked daily at 04:00 UTC, the last 10 business days released when anything is new or changed. Coverage: First release: the Reserve Bank's rand exchange rates, rand per US dollar, British pound, euro and Japanese yen, and the nominal effective exchange rate of the rand (index, 2022 = 100), every business day from 4 January 2016 to 30 October 2026. Still to add: SARB's interest rate, money, credit and balance-of-payments series, a few at a time, each checked the same way.
/v1/datasets/reserve-bank-series/records · /latest gives the latest business day of each currency
| Filter | Example | Meaning |
|---|---|---|
currency | USD | USD, GBP, EUR, JPY, or NEER for the nominal effective exchange rate |
from, to | 2026-01-01 | Dates, on date |
Fields (9)
| Field | Type | Meaning |
|---|---|---|
id | string | Currency and date (usd-2026-10-30) |
currency | enum | USD, GBP, EUR or JPY (ISO 4217), or NEER for the nominal effective exchange rate of the rand |
name | string | The currency's or the index's name |
date | date | Business day of the rate, as the Reserve Bank's series shows it |
value | number | Rand per one unit of the currency, the weighted average of the banks' rates at about 10:30 (the yen too: rand per one yen, to four decimals); for NEER the index, where higher means a stronger rand |
unit | string | Rand per US dollar, British pound, euro or Japanese yen, or the index and its base year (Index, 2022 = 100) |
series_code | string | The Reserve Bank's time series code (EXCX135D) |
flags | array | large_move (4% or more from the series' previous value), after_gap (no value on the business day before, though the other series have one) and suspect_value_in_source (far from both neighbouring days, published as SARB shows it) |
source | object | Publisher, series and the API address it came from |
curl "https://api.unmappedlabs.com/v1/datasets/reserve-bank-series/records?currency=USD" \
-H "Authorization: Bearer um_live_your_key"