ብርBirrwatch API documentation · v1 ← Birrwatch.et /api/rates /api/trends

Free Ethiopian exchange-rate data

Two public JSON endpoints give you every rate Birrwatch collects — 27+ commercial banks, 5 licensed FX bureaus, NBE's official reference, the customs valuation rate and a parallel-market (USDT/ETB) indicator — updated automatically three times a day, plus daily history. No key, no signup. Free with attribution.

Live — from the current dataset
Sources…
Quotes…
Updated…
USD bank avg…

Loading live values…

Endpoints

GET /api/rates static json

The full current dataset: every collected source and currency, plus per-source freshness. Updates 3×/day (≈ 08:23, 11:23, 14:23 Addis time).

FieldTypeMeaning
meta.generated_atstringDataset build time (ISO-8601, UTC). Compare before re-processing — if unchanged, skip.
meta.versionnumberContract version — currently 1. Fields are only ever added, never renamed.
meta.disclaimerstringData provenance & no-warranty text.
sources.<ID>.namestringDisplay name, e.g. "Commercial Bank of Ethiopia".
sources.<ID>.typestringbank · bureau · official (NBE) · customs (ERCA) · market (parallel USDT).
sources.<ID>.fetched_atstringWhen that source was last read. Failed sources keep their previous stamp.
rates[].sourcestringSource ID — joins with sources.
rates[].currencystringISO code. USD, EUR, AED, SAR, GBP, CNY across banks; USDT for the parallel reference.
rates[].buynumberRate at which the source buys FX from you (you receive ETB). Higher is better when selling.
rates[].sellnumberRate at which the source sells FX to you (you pay ETB). Lower is better when buying.
rates[].flagstring?"stale" when the sheet failed our fleet cross-check (see Semantics). Optional field.
stale_since.<src|ccy>string?Date the sheet was first flagged stale. Optional.

GET /api/trends static json

Daily history, one value per calendar day, arrays aligned to dates. Grows daily — collecting since September 2026.

FieldTypeMeaning
dates[]string[]ISO dates, ascending. Last element = latest collected day.
series.<CCY>.mid[]number[]Daily bank-average mid for that currency. Gaps forward-filled from the previous day.
series.<CCY>.official[]number[]?Daily NBE official weighted average (published for USD, EUR, AED, SAR, GBP, CNY on weekdays).

Semantics you should know

buy vs sellAlways from the customer's perspective at that counter. A bank "buys" your dollars; it "sells" dollars to you. Official and customs sources publish one reference number — we store it as buy = sell.
stale flagWhen one bank's sheet fails our cross-check against the median of ~25 banks (>2.5% off for USD, >4% for others), we flag it "stale" and keep serving it — but the Birrwatch site excludes it from averages, "best rate" ranking and alerts. You should probably do the same; the flag exists so you can decide.
USDT / ETBAn indicative parallel-market reference from public sources — not a bank or NBE rate, not an offer to trade.
NBE officialThe published "Indicative Daily Exchange Rate" (weighted average of the previous day's bank transactions). Published weekdays only; buy = sell.
Missing rowsIf a bank didn't publish a currency today, the row is absent — never zero, never invented.

Usage rules

1. Cache for at least 15 minutes — the dataset updates 3×/day; polling faster gains nothing.

2. Attribute "Rates via Birrwatch" with a link to birrwatch.et.

3. Never present the data as NBE-official. Rates are indicative aggregates of published sheets, provided as-is with no warranty of accuracy or availability.

Examples — with live values

The values inside these examples were fetched from the live API when you loaded this page.

curl

curl https://birrwatch.et/api/rates

Python — bank-average USD/ETB right now

import requests d = requests.get("https://birrwatch.et/api/rates").json() banks = [r for r in d["rates"] if r["currency"] == "USD" and d["sources"][r["source"]]["type"] == "bank" and not r.get("flag")] avg = sum((float(r["buy"]) + float(r["sell"])) / 2 for r in banks) / len(banks) print(f"USD/ETB bank average: {avg:.2f}")
This code just printed
…

JavaScript — best USD buy across banks

const d = await fetch("https://birrwatch.et/api/rates").then(r => r.json()); const best = d.rates .filter(r => r.currency === "USD" && d.sources[r.source]?.type === "bank" && !r.flag) .sort((a, b) => b.buy - a.buy)[0]; console.log(best.source, "buys USD at", best.buy);
This code just printed
…

Live sample — USD quotes as served right now

sourcetypebuysellflag
loading…

First bank rows of the current dataset. Fetch /api/rates for all of it.