Skip to content
Python guide

Exchange rate API in Python: a complete integration guide

Fetch live and historical exchange rates, convert currencies with Decimal, load time series into pandas and handle errors and caching – with plain requests and httpx code you can run today.

Last updated: · fxapi team

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

  1. Create a free account and copy your API key.
  2. Store it in an environment variable – never commit it:
export FXAPI_KEY="your-api-key"
pip install requests httpx pandas
EndpointWhat it returnsPlans
/v1/latestLatest rates for 190+ currenciesAll
/v1/historicalEnd-of-day rates for a date since 1999-01-01All
/v1/convertConverted amountsBasic and up
/v1/rangeTime series, JSON or CSVProfessional and up
/v1/averageMonthly/quarterly/yearly averagesAll

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

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?
Call /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?
Either the monthly quota is used up or, on the Free plan, you sent more than 10 requests per minute. Check the X-RateLimit-Remaining-Quota-Month header and cache responses – see caching exchange rates.
Can I load exchange rates straight into a pandas DataFrame?
Yes. /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?
Yes, the 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.
Free plan · no credit card

Get your free exchange rate API key

300 requests a month, latest and historical rates, fluctuation and averages – free forever. Upgrade when you need faster updates or more requests.