This guide shows how to use an exchange rate API in Python with fxapi: fetch the latest rates, look up a historical rate for a date, do currency conversion in Python with Decimal, pull a time series into pandas, and handle errors and caching properly. All examples use plain HTTPS calls with the apikey header, so they work with any HTTP client.
Before you start
- Create a free account and copy your API key.
- Store it in an environment variable – never commit it:
export FXAPI_KEY="your-api-key"
pip install requests httpx pandas
| Endpoint | What it returns | Plans |
|---|---|---|
/v1/latest | Latest rates for 190+ currencies | All |
/v1/historical | End-of-day rates for a date since 1999-01-01 | All |
/v1/convert | Converted amounts | Basic and up |
/v1/range | Time series, JSON or CSV | Professional and up |
/v1/average | Monthly/quarterly/yearly averages | All |
An exchange rate API client in Python with error handling
Wrap every call in one function so status codes are handled in a single place:
import os
import requests
BASE_URL = "https://api.fxapi.com/v1"
session = requests.Session()
session.headers.update({"apikey": os.environ["FXAPI_KEY"]})
class FxApiError(Exception):
def __init__(self, status, message, errors=None):
super().__init__(f"{status}: {message}")
self.status = status
self.errors = errors or {}
def fx_get(path, **params):
resp = session.get(f"{BASE_URL}{path}", params=params, timeout=10)
if resp.status_code == 200:
return resp
try:
body = resp.json()
except ValueError:
body = {}
if resp.status_code == 401:
raise FxApiError(401, "Invalid API key – check FXAPI_KEY")
if resp.status_code == 403:
raise FxApiError(403, "Endpoint or option not included in your plan")
if resp.status_code == 422:
raise FxApiError(422, "Validation error", body.get("errors"))
if resp.status_code == 429:
left = resp.headers.get("X-RateLimit-Remaining-Quota-Month")
raise FxApiError(429, f"Rate limit reached (monthly quota left: {left})")
raise FxApiError(resp.status_code, resp.text[:200])
A 422 body contains an errors object keyed by parameter name, so err.errors tells you exactly which argument was wrong. Validation errors and server errors don’t count against your quota.
Get the latest exchange rates in Python
def latest_rates(base="USD", currencies=None):
params = {"base_currency": base}
if currencies:
params["currencies"] = ",".join(currencies)
payload = fx_get("/latest", **params).json()
rates = {code: item["value"] for code, item in payload["data"].items()}
return payload["meta"]["last_updated_at"], rates
updated_at, rates = latest_rates("EUR", ["USD", "GBP", "JPY"])
print(updated_at, rates["USD"])
Each value is the amount of the target currency for one unit of the base currency. meta.last_updated_at is the UTC timestamp of the data – store it alongside anything you calculate. How often it changes depends on your plan: daily on Free, hourly on Basic, every 60 seconds on Professional and Enterprise (pricing).
Historical exchange rate on a date
payload = fx_get(
"/historical", date="2024-12-31", base_currency="USD", currencies="EUR,CHF"
).json()
print(payload["data"]["EUR"]["value"])
date must be at least one day in the past and not earlier than 1999-01-01. Values are end-of-day rates in UTC. See the historical exchange rates API for details.
Currency conversion in Python
On Basic and higher plans, /v1/convert returns converted amounts directly:
payload = fx_get("/convert", value=250, base_currency="USD", currencies="EUR,GBP").json()
print(payload["data"]["EUR"]["value"]) # 250 USD expressed in EUR
On the Free plan – or to avoid an extra request per conversion – compute it from the latest rates. Use Decimal, not float, for money:
from decimal import Decimal, ROUND_HALF_EVEN
_, rates = latest_rates("USD", ["EUR"])
amount = Decimal("250.00")
converted = (amount * Decimal(str(rates["EUR"]))).quantize(
Decimal("0.01"), rounding=ROUND_HALF_EVEN
)
Why Decimal(str(...)) and how many decimal places each currency needs is covered in currency rounding and precision.
Async requests with httpx
When you need many historical dates, httpx.AsyncClient fetches them concurrently. A semaphore caps concurrency; on the Free plan, which allows 10 requests per minute, also keep each batch below that limit:
import asyncio
import os
import httpx
async def historical_many(dates, base="USD", currencies="EUR"):
sem = asyncio.Semaphore(5)
async with httpx.AsyncClient(
base_url="https://api.fxapi.com/v1",
headers={"apikey": os.environ["FXAPI_KEY"]},
timeout=10,
) as client:
async def one(day):
async with sem:
r = await client.get(
"/historical",
params={"date": day, "base_currency": base, "currencies": currencies},
)
r.raise_for_status()
return day, r.json()["data"]
return dict(await asyncio.gather(*(one(d) for d in dates)))
result = asyncio.run(historical_many(["2026-01-02", "2026-02-02", "2026-03-02"]))
If you need more than a handful of dates, one /v1/range request is cheaper than many /v1/historical calls.
pandas time series from the range endpoint
/v1/range (Professional and up) returns a daily, hourly or even minute-level series. With format=csv it loads straight into pandas. CSV columns are datetime,base_currency,currency,value:
import io
import pandas as pd
resp = fx_get(
"/range",
datetime_start="2026-01-01T00:00:00Z",
datetime_end="2026-06-30T23:59:59Z",
accuracy="day",
base_currency="USD",
currencies="EUR,GBP,JPY",
format="csv",
)
df = pd.read_csv(io.StringIO(resp.text), parse_dates=["datetime"])
wide = df.pivot(index="datetime", columns="currency", values="value")
print(wide.pct_change().describe()) # daily returns
print(wide.rolling(30).mean().tail()) # 30-day moving average
With accuracy=day the span can be up to 366 days; use accuracy=month for month-end values over longer periods. On the Free plan, /v1/average?format=csv gives monthly averages instead – see the time series API and average rates API.
Caching exchange rates in Python
Historical rates never change, so cache them forever. Latest rates only change as often as your plan updates, so cache them for that interval and serve every user from the cache:
import time
from functools import lru_cache
_latest = {"at": 0.0, "data": None}
TTL_SECONDS = 3600 # Basic plan: hourly updates
def cached_latest():
if _latest["data"] and time.monotonic() - _latest["at"] < TTL_SECONDS:
return _latest["data"]
try:
_latest["data"] = latest_rates("USD") # one request, all currencies
_latest["at"] = time.monotonic()
except (FxApiError, requests.RequestException):
if _latest["data"] is None:
raise # nothing to fall back to
return _latest["data"]
@lru_cache(maxsize=4096)
def cached_historical(day, base="USD"):
return fx_get("/historical", date=day, base_currency=base).json()["data"]
Request all currencies once against a single base and derive cross rates locally – that turns hundreds of possible calls into one. In multi-process deployments, move the cache to Redis; the caching guide has a full example.
Using the official Python SDK
The fxapicom package wraps the same endpoints and returns parsed dictionaries. Note that currencies is a Python list:
pip install fxapicom
import os
import fxapicom
client = fxapicom.Client(os.environ["FXAPI_KEY"])
latest = client.latest(base_currency="EUR", currencies=["USD", "GBP"])
hist = client.historical("2024-12-31", base_currency="EUR", currencies=["USD"])
print(client.status()) # quota usage, does not count against the quota
The SDK raises exceptions from its everapi.exceptions module (for example IncorrectApikey, NotAllowed, QuotaExceeded, RateLimitExceeded), so you can catch them like the FxApiError above.
Next steps
- Check quota usage any time with
/v1/status– it’s free. - Read the full API documentation or grab the OpenAPI spec to generate a typed client.
- Working in another stack? See the JavaScript guide or all integration guides.
The examples in this guide use fxapi, a foreign exchange rate API with a free plan of 300 requests a month – no credit card required.
Frequently asked questions
How do I convert currency in Python without a paid plan?
/v1/latest with base_currency set to your source currency and multiply the amount by the target rate using Decimal. The /v1/convert endpoint does the multiplication for you but requires the Basic plan or higher.Should I use requests or httpx for an exchange rate API in Python?
requests is the simplest choice for scripts and synchronous web apps. Use httpx.AsyncClient in asyncio code such as FastAPI services, or when you fetch many historical dates concurrently.Why does my Python code get HTTP 429?
X-RateLimit-Remaining-Quota-Month header and cache responses – see caching exchange rates.Can I load exchange rates straight into a pandas DataFrame?
/v1/range, /v1/average and /v1/fluctuation accept format=csv, so pd.read_csv(io.StringIO(resp.text)) gives you a tidy DataFrame in one line. /v1/range requires the Professional plan; /v1/average works on every plan.Is there an official Python SDK?
fxapicom package on PyPI. Plain requests code gives you more control over timeouts, retries and caching, which is why this guide uses it for most examples.