# Modan API — Complete Reference for LLMs and AI Agents > Modan provides live and historical African FX / remittance pricing at the > provider level (e.g. GBP→NGN across named providers such as banks, IMTOs and > fintechs), benchmarked against an independent mid-market reference where > available. This file is the complete machine-readable reference: everything > an agent needs to authenticate, call every endpoint, and interpret every > field. Human docs: https://modan.io/docs/api · OpenAPI 3.1: > https://modan.io/openapi.json ## Authentication - Get a key: sign up free at https://modan.io/signup — the first API key is minted automatically (shown once; keys look like `mdn_live_` + 40 chars). Manage keys at https://modan.io/app/api. - Every REST request: pass the key in the `X-API-Key` header. - Missing/invalid/revoked key → `401 {"error": "Invalid or missing API key"}`. ## Quotas & data freshness (per account per day, reset 00:00 UTC) | Tier | Requests/day | Data freshness | | ---------- | ------------ | -------------- | | free | 50 | hourly | | pro | 250 | hourly | | enterprise | 1,000 | real-time | - Quota is shared across all of an account's keys and includes MCP tool calls. - Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` (epoch seconds of next UTC midnight). - Exceeding the quota → `429 {"error": "Rate limit exceeded", "limit": N, "reset": epoch}`. - Data freshness: hourly tiers (free, pro) serve rates as of the top of the current UTC hour — rate responses include `data_freshness: "hourly"` and an `as_of` timestamp, plus an `X-Data-Freshness: hourly` header. Team-plan keys (tier id `enterprise`) serve every observation in real time (`data_freshness: "realtime"`). The delay is always stated explicitly, never silent. ## MCP server (preferred for AI agents) - Endpoint: `https://modan.io/api/mcp` — MCP streamable HTTP, stateless (plain JSON responses, no SSE stream or session ids). JSON-RPC 2.0. - Auth: `X-API-Key` header, or `Authorization: Bearer mdn_live_...`. `initialize` / `tools/list` / `ping` are open; `tools/call` requires a key and consumes one request of the daily quota. - Tools (same data as REST): `get_rates(from,to)`, `convert(from,to,amount)`, `fetch_rates(base, quotes[])` (mid-market rates for one base against ≤20 quotes, plus best tracked provider per covered corridor), `get_history(from,to,days?,provider?)`, `list_corridors()`, `list_providers()`, `list_currencies()`. - Tool failures come back as `isError: true` results with an actionable message (how to get a key, fix a currency code, when quota resets). - Claude Code: `claude mcp add --transport http modan https://modan.io/api/mcp --header "X-API-Key: mdn_live_YOUR_KEY_HERE"` - Generic MCP client config: `{"mcpServers": {"modan": {"type": "http", "url": "https://modan.io/api/mcp", "headers": {"X-API-Key": "mdn_live_YOUR_KEY_HERE"}}}}` ## REST endpoints Base URL: `https://modan.io/api/v1`. All read endpoints are GET and require `X-API-Key` (exceptions: `/status` needs no key; `/admin/usage` and `/status` never consume quota); the one write endpoint is POST /rates (admin/treasury keys only). All responses are JSON. Currency codes are uppercase, 3-letter ISO-4217 fiat plus 4–5 char stablecoins (GBP, USD, NGN, KES, GHS, XOF, USDT, USDC, ...). Unknown `/api/*` paths return a JSON 404 (never HTML). ### GET /rates?from=GBP&to=NGN Latest rate from every provider quoting the corridor. Response: { "corridor": "GBP/NGN", "providers": [ { "provider_id": "wise", "provider_name": "Wise", "rate": 2045.50, // target units per 1 source unit "fee": 2.99, // provider fee, may be null "fee_currency": "GBP", // currency of the fee, may be null "spread_bps": 24.5, // basis points BELOW the best provider (0 = best) "vs_mid_bps": -12.3, // vs independent mid (present only when mid exists) "transfer_time": "1 - 2 business days", "last_updated": "2026-07-06T09:30:00.000Z" } ], "count": 1, "mid_rate": 2048.02, // independent mid-market reference (optional) "mid_source": "open.er-api.com", "mid_fetched_at": "2026-07-06T09:05:00.000Z", "timestamp": "2026-07-06T09:30:05.000Z" } Semantics: - `spread_bps` is dispersion vs the best provider in the corridor at that moment OF THE SAME `rate_type`, NOT vs mid. `vs_mid_bps` is vs the independent mid (negative = provider pays out less than mid, which is typical). - `rate_type` is what kind of price it is — official | interbank | retail | p2p | parallel — and `provider_type` is what kind of institution published it. They are orthogonal. A central bank's `official` reference is real and not obtainable, so it is excluded from `best` on /convert and from `best_rate` on /corridors; it is still returned, labelled. Absent `rate_type` means `retail`. - The mid is cross-computed through USD from the reference feed, so every fiat corridor we track is priced; a crossed mid is timestamped with its staler leg. Currencies the feed does not quote (stablecoins: USDT, USDC) have none. - `mid_*` fields and `vs_mid_bps` are omitted entirely when no recent (≤48h) reference exists — absence is explicit, never fabricated. - 400 if `from`/`to` missing. ### GET /convert?from=GBP&to=NGN&amount=1000 What an amount actually delivers, per provider, net of fees. Response: { "from": "GBP", "to": "NGN", "amount": 1000, "mid": { "rate": 2048.02, "converted": 2048020, "source": "...", "fetched_at": "..." }, // or null "best": { /* the provider entry with the highest net_converted */ }, "providers": [ { "provider_id": "wise", "provider_name": "Wise", "rate": 2045.50, "fee": 2.99, "fee_currency": "GBP", "converted": 2045500, // amount * rate, before fees "net_converted": 2039382.55, // converted minus fee expressed in target currency "spread_bps": 0, "vs_mid_bps": -12.3 } ], "count": 1, "timestamp": "..." } Semantics: `best` = highest `net_converted` (best value to the recipient), which is NOT always the highest headline rate once fees count. Fees quoted in the source currency are converted at that provider's rate. ### GET /fetch-one · /fetch-multi · /fetch-matrix · /fetch-many-to-one fastforex-style convenience lookups. Every pair returns the independent MID-MARKET rate (cross-computed through the freshest USD reference snapshot, ≤48h old) plus `provider_best` — the best tracked provider rate — when the pair is a covered corridor (null otherwise). One call = ONE quota unit regardless of pair count. - GET /fetch-one?from=USD&to=NGN → { base, quote, mid, provider_best, source, fetched_at, timestamp } - GET /fetch-multi?from=USD&to=NGN,KES,GHS (≤20 quotes) → { base, results: { NGN: { mid, provider_best }, ... }, count, source, fetched_at, timestamp } - GET /fetch-matrix?from=USD,GBP&to=NGN,KES (≤10×10) → { bases, quotes, results: { USD: { NGN: {...}, ... }, ... }, ... } - GET /fetch-many-to-one?from=USD,GBP,CAD&to=NGN → { quote, results: { USD: {...}, GBP: {...}, CAD: {...} }, count, ... } `provider_best` = { rate, provider_id, last_updated }. Same-currency pairs return mid = 1 with provider_best null. Unsupported currencies → 400 { error, invalid: [...], supported: [...] } — only currencies the reference feed quotes are cross-computable (see /currencies for the live list). ### GET /time-series?from=GBP&to=NGN&period=30d&interval=P1D Bucketed series combining the best tracked provider rate and the independent mid per bucket. - `interval`: P1D (daily, default, ≤366 buckets) | PT1H (hourly, ≤168) - Window: `period` (1d|7d|30d|90d, default 30d) OR explicit `start`/`end` ISO dates. Over-long windows are clamped (a `note` says so). - Response: { corridor, interval, start, end, tracked_corridor, data: [{ t, mid, best, samples }], count }. `best` = max provider rate in the bucket; `samples` = observations in the bucket; `mid` = last reference mid in the bucket (null before the reference feed's history begins). - Untracked pairs return a mid-only series with an explanatory `note`. ### GET /historical?from=GBP&to=NGN&date=2026-07-01 Corridor snapshot as of end-of-day UTC on `date` (YYYY-MM-DD, not future): same shape as /rates (each provider's latest quote ≤ that date within the prior 7 days) plus `date` and `as_of`. `mid_*` fields appear only when the reference feed covers that date. ### GET /change?from=GBP&to=NGN&period=7d Movement over a period (1d|7d|30d|90d, default 7d): { corridor, period, start: { at, mid, best }, end: { at, mid, best }, change: { mid: { abs, pct }, best: { abs, pct } } }. Legs with no data are null (e.g. mid before the reference feed existed). ### GET /rates/provider?provider=lemfi Every corridor and current rate one provider quotes: { provider: { id, name, provider_type, region, transfer_time, website_url }, corridors: [{ from, to, rate, fee, fee_currency, last_updated }], count }. Unknown/inactive provider → 404 with guidance to GET /providers. ### GET /admin/usage Your account's metering state — does NOT consume quota: { plan, period_start, period_end, limit, used, remaining, reset, key: { id, name }, keys_today: [{ key_id, name, requests }], daily_history: [{ date, requests }] }. Quota is per-account per UTC day. ### GET /status (no key, no quota) Public platform health: { status, version, corridors, providers, last_rate_update, mid_feed: { source, last_fetched, age_seconds }, timestamp }. Safe to poll for monitoring — cached 30s, never counted. ### GET /rates/history?from=GBP&to=NGN&period=30d&order=asc&limit=500&offset=0 Historical time series of provider observations. - `period`: 1d | 7d | 30d (default) | 90d - `order`: asc (default, oldest-first) | desc - `limit`: ≤ 5000 (default 500); `offset` for paging - `provider`: optional provider_id filter - Response: { corridor, period, from_date, to_date, order, limit, offset, has_more, data: [{ timestamp, provider_id, provider_name, rate, fee, fee_currency, spread_bps }], count }. Use `has_more` to page. ### GET /currencies Active currencies and the corridors currently served. Response: { currencies: [{ code, name, type, flag_url, is_active, sort_order }], corridors: [{ from, to }], currency_count, corridor_count, timestamp }. `type` is "source" | "target" | "both". ### GET /corridors All covered corridors with provider counts and current best rates. ### GET /providers Active provider metadata: id, name, provider_type, rate_type, region, transfer_time, website_url, payment methods. provider_type is the institution: central_bank | commercial_bank | non_bank_lp | imto | fintech_psp | crypto_venue | bureau_de_change | aggregator. rate_type is the KIND of price it publishes: official | interbank | retail | p2p | parallel. The two are orthogonal — a commercial bank may post a retail board rate or an interbank one. ### POST /rates (data-team only) Ingest rate observations programmatically. Requires an API key whose OWNING ACCOUNT holds the admin or treasury role — regular data keys get 403. Body: one object or an array (≤100) of: { "provider_id": "wise", "from": "GBP", "to": "NGN", "rate": 2045.5, "fee": 2.99, "fee_currency": "GBP", "notes": "...", "effective_from": "..." } - `from`/`to` aliases for source_currency/target_currency; fee/notes/ effective_from optional (effective_from defaults to now; max 5y backfill, no future timestamps). - The batch is ATOMIC: any invalid row → 422 with per-row errors and nothing inserted. Success → 201 {"inserted": n}. - Ingestion does NOT consume the daily read quota. ## Errors | Status | Meaning | | ------ | ------------------------------------------------------ | | 400 | Missing/invalid query params (message says which) | | 401 | API key missing, invalid, or revoked | | 404 | Unknown endpoint (JSON body, lists valid endpoints) | | 429 | Daily quota exceeded (body carries limit + reset epoch) | | 500 | Server error — retry after a moment | All error bodies are JSON: `{"error": "..."}` with an actionable message. ## Caching Responses send `Cache-Control: private, max-age=15` (rates/convert) to `max-age=300` (providers/currencies). Live rates change intraday; do not cache beyond those windows. ## Public pages (real HTML, no JavaScript needed) - https://modan.io/corridors — every corridor with the best executable quote. - https://modan.io/currency/{code} — one page per currency, e.g. /currency/ngn. - https://modan.io/{from}/{to} — one page per corridor, e.g. /gbp/ngn. - https://modan.io/{provider_id}/{from}/{to} — one page per provider per corridor. - https://modan.io/providers and /providers/{provider_id}. - https://modan.io/docs/mcp — MCP setup for Claude Code, Cursor, VS Code and others. - https://modan.io/sitemap.xml — every public URL, with last-modified times. ## Links - Human docs: https://modan.io/docs/api - OpenAPI 3.1: https://modan.io/openapi.json - Index for LLMs: https://modan.io/llms.txt - Pricing/tiers: https://modan.io/pricing - Sign up (free key, auto-minted): https://modan.io/signup