Developers
Get naira (NGN) exchange rates by API in Python and JavaScript
Most exchange-rate APIs give you one number per pair — a mid nobody transacts at. For African corridors that number is not the market. This walkthrough fetches what named providers actually quote for GBP→NGN, with spread and an independent mid, in a few lines of Python and JavaScript.
Updated 28 Sep 2026 · 7 min read · by Modan
1. Get a key
Sign up — no card. Your first key is minted automatically and shown under API Keys → My Keys. Your daily allowance is 25 per UTC day on Free, 250 on Pro and 5,000 on Max; it is per person, and every key you hold, MCP and the terminal draw on the same one. Every request carries the key in the X-API-Key header.
2. The request
One call returns the latest quote of every tracked provider on the corridor of the types your key's plan reads (IMTOs and fintechs & PSPs on a free key, every type on Pro and Max), each with rate, fee, provider_type, spread_bps (distance below the best current rate of the same kind: the same provider type and rate type), stale (true once the quote has gone 24 hours without a check, in which case spread_bps is null) and last_checked (when it was last confirmed), plus mid_rate and per-provider vs_mid_bps when the independent mid is available. The response also states its freshness: data_freshness is hourly on Free and Pro keys (with an as_of timestamp) and realtime on Max keys.
Add &provider_type=imto to get only the money-transfer operators, or a comma list such as imto,fintech_psp; leave it out for every type your key's plan reads (plans and provider types). An unknown type is refused with 400 before the key is checked, so a typo costs no quota.
curl "https://modan.io/api/v1/rates?from=GBP&to=NGN" \ -H "X-API-Key: mdn_live_YOUR_KEY_HERE"
3. Python
Print the current quotes ranked by spread within each kind (provider type and rate type), the stale ones separately with the time they were last checked, and the mid if there is one.
import os, requests
BASE = "https://modan.io/api/v1"
HEADERS = {"X-API-Key": os.environ["MODAN_API_KEY"]}
r = requests.get(f"{BASE}/rates", params={"from": "GBP", "to": "NGN"}, headers=HEADERS, timeout=15)
r.raise_for_status()
book = r.json()
print(book["corridor"], "as of", book.get("as_of") or book["timestamp"], f"({book['data_freshness']})")
if book.get("mid_rate"):
print("independent mid:", book["mid_rate"], "from", book["mid_source"])
# A stale quote (not checked for 24 hours) has spread_bps = None: list it, never rank it.
current = [p for p in book["providers"] if not p["stale"]]
stale = [p for p in book["providers"] if p["stale"]]
# spread_bps is measured within a provider type and rate type, so each kind is ranked on its own.
def kind(p):
return (p["provider_type"] or "~", p["rate_type"]) # no type recorded: null, sorted last
for p in sorted(current, key=lambda p: (*kind(p), p["spread_bps"])):
fee = "n/a" if p["fee"] is None else p["fee"]
vs_mid = f'{p["vs_mid_bps"]:+.1f} bps vs mid' if "vs_mid_bps" in p else "no mid"
print(f'{p["provider_name"]:<16} {p["provider_type"] or "-":<15} {p["rate_type"]:<9} {p["rate"]:>12,.2f} fee {fee:>6} {p["spread_bps"]:>6.1f} bps {vs_mid}')
for p in stale:
print(f'{p["provider_name"]:<16} {p["provider_type"] or "-":<15} {p["rate_type"]:<9} {p["rate"]:>12,.2f} stale, last checked {p["last_checked"]}')
print("remaining today:", r.headers.get("X-RateLimit-Remaining"))4. JavaScript
The same call with fetch, in Node 18+ or the browser, narrowed to money-transfer operators with provider_type=imto, picking the best current executable quote among them.
const BASE = "https://modan.io/api/v1";
const headers = { "X-API-Key": process.env.MODAN_API_KEY };
// Money-transfer operators only, so the best is chosen like for like.
const res = await fetch(`${BASE}/rates?from=GBP&to=NGN&provider_type=imto`, { headers });
if (res.status === 429) throw new Error(`quota exhausted until ${res.headers.get("X-RateLimit-Reset")}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const book = await res.json();
// Only a current, executable quote can be the best: a stale one (not checked for 24 hours) is listed, never ranked.
const eligible = book.providers.filter((p) => !p.stale && !["official", "parallel"].includes(p.rate_type ?? "retail"));
if (eligible.length === 0) {
console.log(`${book.corridor}: no current executable IMTO quote (${book.stale_count} stale)`);
} else {
const best = eligible.reduce((a, b) => (b.rate > a.rate ? b : a));
console.log(`${book.corridor}: best current IMTO ${best.provider_name} ${best.rate}, checked ${best.last_checked} (${book.data_freshness})`);
}5. Convert an amount, net of fees
/convert?from=GBP&to=NGN&amount=1000 returns, for every provider of the types your key's plan reads, the gross converted amount and the amount net of the provider's fee, and names the provider that delivers the most — among current executable quotes only — overall in best and within each provider type in best_by_type. A central bank's official reference and a stale quote are returned and labelled but never chosen as best, and best is null when no quote qualifies; so is a type's entry in best_by_type when none of that type does.
6. History and quotas
/rates/history?from=GBP&to=NGN&period=30d returns timestamped observations (oldest first, paginated); /time-series gives the mid and the best current executable quote at the end of each day or hour; /historical?date=… returns the book as it stood on a past date. Each takes provider_type as well. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset; past the daily limit you receive 429 until midnight UTC. /status needs no key and does not count.
If your consumer is an AI agent rather than a script, the MCP server exposes the same data as tools, on the same key and quota.
Frequently asked questions
- Is there a free naira exchange rate API?
- Yes. A Modan key is free on signup, and a Free account allows 25 requests per UTC day, with rates as of the top of the current hour. Paid plans raise the daily allowance to 250 (Pro) and 5,000 (Max, real-time) per person.
- How fresh is the data on the free plan?
- Hourly: responses on Free and Pro keys are as of the top of the current UTC hour and say so in data_freshness and as_of. Max keys receive every observation in real time. How often a provider is checked depends on how its rates are collected — some automatically through the day, others by hand, banks on weekdays only — and every quote carries its last_checked time.
- Which other currencies are covered?
- Every corridor Modan tracks is listed at /corridors — 80 pairs at the time of writing across NGN, GHS, KES, XOF, ZAR, EGP and more, plus USDT and USDC corridors. GET /corridors returns the ones your key's plan reads: on a free key, those IMTOs or fintechs & PSPs quote, so not the USDT and USDC corridors.