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.
Loading live values…
The full current dataset: every collected source and currency, plus per-source freshness. Updates 3×/day (≈ 08:23, 11:23, 14:23 Addis time).
| Field | Type | Meaning |
|---|---|---|
meta.generated_at | string | Dataset build time (ISO-8601, UTC). Compare before re-processing — if unchanged, skip. |
meta.version | number | Contract version — currently 1. Fields are only ever added, never renamed. |
meta.disclaimer | string | Data provenance & no-warranty text. |
sources.<ID>.name | string | Display name, e.g. "Commercial Bank of Ethiopia". |
sources.<ID>.type | string | bank · bureau · official (NBE) · customs (ERCA) · market (parallel USDT). |
sources.<ID>.fetched_at | string | When that source was last read. Failed sources keep their previous stamp. |
rates[].source | string | Source ID — joins with sources. |
rates[].currency | string | ISO code. USD, EUR, AED, SAR, GBP, CNY across banks; USDT for the parallel reference. |
rates[].buy | number | Rate at which the source buys FX from you (you receive ETB). Higher is better when selling. |
rates[].sell | number | Rate at which the source sells FX to you (you pay ETB). Lower is better when buying. |
rates[].flag | string? | "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. |
Daily history, one value per calendar day, arrays aligned to dates. Grows daily — collecting since September 2026.
| Field | Type | Meaning |
|---|---|---|
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). |
| buy vs sell | Always 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 flag | When 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 / ETB | An indicative parallel-market reference from public sources — not a bank or NBE rate, not an offer to trade. |
| NBE official | The published "Indicative Daily Exchange Rate" (weighted average of the previous day's bank transactions). Published weekdays only; buy = sell. |
| Missing rows | If a bank didn't publish a currency today, the row is absent — never zero, never invented. |
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.
The values inside these examples were fetched from the live API when you loaded this page.
curl https://birrwatch.et/api/ratesimport 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}")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);| source | type | buy | sell | flag |
|---|---|---|---|---|
| loading… | ||||
First bank rows of the current dataset. Fetch /api/rates for all of it.