API guide

Exchange rate API: live and historical FX rates, with code

Converting currencies is a three-line problem until someone asks which rate, taken when, from whom. Answer those first and the code stays small.

On this page
  1. Data, not execution: what an exchange rate API does
  2. Bid, ask or mid: which rate to use
  3. Currency conversion in Python, including unlisted pairs
  4. Historical exchange rates by date
  5. Precision, rounding and stale rates
  6. Live rates without polling: the forex stream
  7. Choosing a currency exchange rate API
  8. Questions

Key takeaways

  • An exchange rate API returns market rates as data; it does not convert money or execute trades.
  • A live FX quote has a bid and an ask; the mid, (bid + ask) ÷ 2, is the neutral rate for display and conversion.
  • A pair nobody lists can be crossed through the US dollar: EUR to DOP is EURUSD × USDDOP.
  • Historical exchange rates come from daily bars; for a weekend or holiday, use the last business-day close before it.
  • TickerLayer serves 950+ currency pairs over REST and WebSocket, and the free tier includes 3,000 REST requests a month.

An exchange rate API returns the price of one currency in another over HTTP: EURUSD at 1.1373 means one euro buys 1.1373 US dollars. A useful one gives you three things: a live quote with bid and ask, historical daily rates by date, and a stream for the moment polling gets expensive. It is data, not execution: it does not convert, send or settle money.

This guide belongs to our market data API series. It covers which rate to use, conversion code that copes with pairs nobody lists, historical rates by date, and when to switch to WebSocket. Every snippet on this page was run against the live API on 28 September 2026, and the outputs shown are what it printed.

One live FX quoteShell
curl -sS "https://api.tickerlayer.com/forex/quote/EURUSD" \
  -H "x-api-key: $TICKERLAYER_API_KEY"

What comes back

{
  "symbol": "EURUSD",1
  "bid": 1.137317,2
  "ask": 1.137367,3
  "bid_size": 57,
  "ask_size": 91,
  "timestamp": 17905925960014
}
  1. symbolSix letters, base currency first. The price is euros quoted in dollars.
  2. bidWhat the market pays for one euro. You receive this when you sell euros.
  3. askWhat the market asks for one euro. You pay this when you buy euros.
  4. timestampUnix milliseconds, UTC: 10:49:56 on 28 September 2026.
GET /forex/quote/EURUSD, captured live. REST numerics are JSON numbers. The spread is 0.00005, half a pip.

Data, not execution: what an exchange rate API does

Search for an exchange rate API and three different products come back mixed together. Data APIs return rates. Broker APIs place trades against their own prices. Payment APIs convert real money at their rate plus a margin. All three return a number called a rate, which is why they get confused in architecture reviews.

FeatureExchange rate data APIBroker trading APIPayment conversion API
ReturnsQuotes, bars, streamsFills and positionsA converted amount
Moves money
Rate you seeMarket bid, ask, midThe broker priceProvider rate plus margin
Typical useDisplay, estimates, analytics, alertsTradingCheckout, payouts, treasury
Needs a funded account
TickerLayer is the first column: derived, indicative market rates for software, not a venue that fills orders.

The practical consequence: if your product settles real money, the rate your bank or payment provider executes at is the one that counts. Use market data to display, estimate and audit that rate, not to replace it.

Bid, ask or mid: which rate to use

Every live FX quote is two prices. The bid is what buyers pay for the base currency, the ask is what sellers want, and the gap is the spread. The mid, halfway between, is the rate most converters show. The bid-ask spread explainer covers why that gap widens at night and at the daily rollover.

RateWhat it isUse it for
BidBest price a buyer pays for the base currencyValuing currency you would sell
AskBest price a seller acceptsCosting currency you would buy
Mid(bid + ask) ÷ 2Display, conversion estimates, analytics
Daily closeLast price of a UTC day barHistorical series, charts, reports
FixingOne rate a benchmark publishes at a set time each dayContracts or accounting rules that name it
A market data API gives you the first four. A fixing is a separate publication; if a contract names one, use that source.

For a converter in your interface, use the mid. For a quote you intend to honour, start from the side your customer trades on and add your margin explicitly, so the fee lives in code rather than in a rounding choice. Spreads are counted in pips: the EURUSD quote above is 0.5 pips wide, and the pip explainer shows why yen pairs count them at the second decimal instead of the fourth.

Currency conversion in Python, including unlisted pairs

A currency converter API call is one multiplication away from a quote, until a user picks a pair nobody lists. TickerLayer lists 950+ pairs, from the majors to the Uruguayan peso, but not every combination of them. The fix is the one dealers use: route through the US dollar, the currency almost every other one is quoted against.

direct: EUR → JPY = EURJPYinverse: JPY → EUR = 1 ÷ EURJPYcross: EUR → DOP = EURUSD × USDDOP

EURJPY
Yen per euro, a listed pair.
1 ÷ EURJPY
Euros per yen, when only the other direction is listed.
EURUSD × USDDOP
Dominican pesos per euro. There is no EURDOP pair, so the dollar bridges it.
Sanity check with live mids at 10:49 UTC: EURUSD 1.137342 × USDJPY 157.0749 = 178.6479, against a listed EURJPY mid of 178.6475. A gap of 0.02 basis points.
convert.pyPython
import os

import requests

BASE_URL = "https://api.tickerlayer.com"
session = requests.Session()
session.headers["x-api-key"] = os.environ["TICKERLAYER_API_KEY"]


def get(path):
    resp = session.get(BASE_URL + path, timeout=10)
    resp.raise_for_status()
    return resp.json()


# Every listed pair, fetched once. Cache it for a day in a real service.
PAIRS = {row["symbol"] for row in get("/forex/symbols")["symbols"]}
_mids = {}


def mid(pair):
    """Mid rate of a listed pair, fetched at most once per run."""
    if pair not in _mids:
        q = get(f"/forex/quote/{pair}")
        _mids[pair] = (q["bid"] + q["ask"]) / 2
    return _mids[pair]


def rate(src, dst):
    """Units of dst that one unit of src buys, plus the route used."""
    if src == dst:
        return 1.0, "same currency"
    if src + dst in PAIRS:
        return mid(src + dst), src + dst
    if dst + src in PAIRS:
        return 1 / mid(dst + src), f"1 / {dst + src}"
    if "USD" in (src, dst):
        raise LookupError(f"no {src}/{dst} pair listed")
    # Cross through the US dollar: src -> USD -> dst.
    to_usd, leg1 = rate(src, "USD")
    from_usd, leg2 = rate("USD", dst)
    return to_usd * from_usd, f"{leg1} x {leg2}"


def convert(amount, src, dst):
    r, route = rate(src.upper(), dst.upper())
    return amount * r, route


for amount, src, dst in [(250, "EUR", "JPY"), (1000, "USD", "EUR"), (500, "EUR", "DOP")]:
    value, route = convert(amount, src, dst)
    print(f"{amount:>8,.2f} {src} = {value:>12,.2f} {dst}   via {route}")
Output, 28 September 2026
  250.00 EUR =    44,651.40 JPY   via EURJPY
1,000.00 USD =       879.31 EUR   via USDEUR
  500.00 EUR =    33,832.52 DOP   via EURUSD x USDDOP

The script makes five requests: the pair list, then one quote for each of the four pairs it actually needs. Two details in the output are worth a second look. USDEUR is itself a listed pair, so the dollar conversion used it directly instead of inverting EURUSD. And the peso conversion took two legs, which means it paid two spreads; prefer a direct pair whenever one exists.

Historical exchange rates by date

Historical exchange rates come from daily bars. GET /forex/agg/{pair}/1/day/{from}/{to} returns open, high, low and close for each UTC day between two dates, both inclusive, paginated with next_offset. The same route serves 1, 5 and 15-minute and 1 and 4-hour bars. Two decisions stay with you: which close counts as the rate for a day, and what to answer for a Saturday.

EURUSD daily closes, 14 to 25 September 2026

USD per EUR

Two business weeks of UTC-day closes. The euro lost about 1.4% against the dollar over the fortnight.TickerLayer GET /forex/agg/EURUSD/1/day, captured 28 September 2026.
history.pyPython
import os
from datetime import date, datetime, timezone

import requests

BASE_URL = "https://api.tickerlayer.com"
HEADERS = {"x-api-key": os.environ["TICKERLAYER_API_KEY"]}


def daily_closes(pair, start, end):
    """{date: close} from UTC daily bars, Monday to Friday, both dates inclusive."""
    path = f"{BASE_URL}/forex/agg/{pair}/1/day/{start}/{end}"
    params = {"sort": "asc", "limit": 5000, "offset": 0}
    closes = {}
    while True:
        resp = requests.get(path, params=params, headers=HEADERS, timeout=15)
        resp.raise_for_status()
        body = resp.json()
        for bar in body["results"]:
            day = datetime.fromtimestamp(bar["t"] / 1000, tz=timezone.utc).date()
            if day.weekday() < 5:  # the FX week runs Sunday evening to Friday evening
                closes[day] = bar["c"]
        if body.get("next_offset") is None:
            return closes
        params["offset"] = body["next_offset"]


def rate_on(closes, day):
    """Close of the last business day on or before `day`."""
    known = [d for d in closes if d <= day]
    if not known:
        raise LookupError(f"no rate on or before {day}")
    return max(known), closes[max(known)]


def monthly_average(closes, year, month):
    values = [c for d, c in closes.items() if (d.year, d.month) == (year, month)]
    return sum(values) / len(values), len(values)


closes = daily_closes("EURUSD", "2026-08-01", "2026-09-26")
used, value = rate_on(closes, date(2026, 9, 26))
print(f"EURUSD for 2026-09-26: {value:.5f} (close of {used})")
avg, n = monthly_average(closes, 2026, 8)
print(f"EURUSD August 2026 average: {avg:.5f} over {n} business days")
Output
EURUSD for 2026-09-26: 1.13913 (close of 2026-09-25)
EURUSD August 2026 average: 1.15918 over 21 business days

One request covers two months, and every question after that is answered from memory. Keeping Monday to Friday gives a clean business-day series and makes the weekend fallback automatic: ask for Saturday 26 September and you get Friday's close. A monthly average rate for an invoice run is then just the mean of those closes.

Daily closes for several pairs are also the raw material for cross-currency analysis, such as a currency correlation matrix in Python. Convert closes to returns first; pips and raw prices are not comparable across pairs.

Precision, rounding and stale rates

Three bugs turn up in almost every homegrown currency converter. The first is floating point: 0.1 + 0.2 is not 0.3 in binary, and a basket converted line by line drifts by a cent. Multiply with a decimal type and round once, at the end, to the target currency's minor unit: two places for euros and dollars, none for yen.

minor_units.pyPython
from decimal import ROUND_HALF_UP, Decimal

# Decimal places of each currency's minor unit (extend for the currencies you sell in).
MINOR_UNITS = {"JPY": 0, "KRW": 0, "EUR": 2, "USD": 2, "GBP": 2, "DOP": 2}


def to_minor(amount: Decimal, currency: str) -> Decimal:
    """Round once, at the end, to the currency's minor unit."""
    step = Decimal(1).scaleb(-MINOR_UNITS.get(currency, 2))
    return amount.quantize(step, rounding=ROUND_HALF_UP)


eurjpy = Decimal("178.6475")  # EURJPY mid at 10:49 UTC, built from a string
print(to_minor(Decimal("250") * eurjpy, "JPY"))                  # 44662
print(to_minor(Decimal("19.99") * Decimal("1.137342"), "USD"))   # 22.74

The second is the weekend. Between the Friday close and the Sunday evening open in New York the FX market is shut, so a quote fetched on Saturday can be a day old and still be the right answer. Show its timestamp instead of implying it is live.

The third is a broken quote on a working day: a bid at or above the ask, a zero, or a rate that has not changed for an hour while London is trading. Keep the last good value, flag the pair, and let the next quote clear it.

Live rates without polling: the forex stream

Polling is cheap until it multiplies. A converter that refreshes 20 pairs every 5 seconds makes 345,600 requests a day, which is more than an Individual plan allows in a month. Try your own numbers:

Polling budget calculator

sec
Requests per second
2.00
Requests per day
57,600
Requests per month
1,267,200
Smallest plan that fits
Business
  • Free422×
  • Individual507%
  • Business5%

One request per symbol per poll. A WebSocket subscription replaces all of these requests with one connection. Quotas are listed on pricing; per-second limits are in the X-RateLimit-Limit header.

Pairs × refreshes × hours. Compare the monthly total with your plan before you ship a polling loop.

A WebSocket connection replaces all of that with one socket. Connect to wss://stream.tickerlayer.com/?apiKey=..., wait for the ready frame, then subscribe:

subscribe messageJSON
{"action": "subscribe", "channels": ["forex.quotes"], "symbols": ["EURUSD", "USDJPY", "EURJPY"]}

A forex.quotes frame

{
  "type": "quote",1
  "channel": "forex.quotes",
  "asset": "forex",
  "symbol": "EURUSD",
  "bid": "1.137107",2
  "ask": "1.137157",
  "bid_size": "44",
  "ask_size": "58",
  "ts": 17905912030023
}
  1. typeSwitch on this first: quote, trade, system or error.
  2. bidA string on the stream, unlike REST. Convert with float() or a decimal type.
  3. tsThe time field on stream frames, Unix milliseconds.
Captured live on 28 September 2026. Same prices as REST, delivered as they change.

The stream follows the FX week, so it goes quiet between the Friday close and the Sunday evening open in New York; our forex market hours guide has the session clock. Reconnects, heartbeats and snapshots are covered in the WebSocket market data guide. For charts that must match a broker terminal tick for tick, the forex docs also describe a single-venue raw line.

Choosing a currency exchange rate API

What to check before you commit

  • Bid and ask, not one number, so you can see the spread and choose a side.
  • The pairs you need, including exotics, or a way to cross them.
  • Daily bars with a stated day boundary (UTC here) and pagination.
  • A timestamp on every quote, in a documented unit.
  • A stream for live screens, so refresh rate is not a billing decision.
  • Readable limits: X-RateLimit-Limit on every response and a documented 429.
  • A licence that matches your use: personal research and commercial display are different plans.

If you are looking for a free currency API, the TickerLayer free tier includes 3,000 REST requests a month with no card, enough to prototype a converter and try the historical endpoints. WebSocket comes with paid plans, and free accounts can request a trial from the dashboard.

Forex feedPriceREST requestsWebSocketUse
Free$03,000 a monthTrial on requestPrototyping
Individual$59 a month250,000 a month1 connection, 10 symbolsPersonal and research
Business$549 a month25,000,000 a month10 connections, unlimited symbolsCommercial use
Yearly billing takes 20% off. Per-second limits differ by plan; read X-RateLimit-Limit on any response.

Pair lists, sample responses and the WebSocket channels are on the forex API product page, and pricing shows the bundles if you also need stocks or crypto on the same key.

Questions

Is there a free exchange rate API?

Yes. The TickerLayer free tier includes 3,000 REST requests a month with no card required. Streaming over WebSocket comes with paid plans, and free accounts can request a trial from the dashboard.

What is the difference between an exchange rate API and a forex trading API?

An exchange rate API returns market rates as data. A forex trading API, offered by brokers, places orders against their prices and needs a funded account. TickerLayer is a data API and does not execute trades.

How do I get the historical exchange rate for a specific date?

Request daily bars for a short window ending on that date, for example GET /forex/agg/EURUSD/1/day/2026-09-19/2026-09-26, and take the close of the last business day on or before it. Weekends and holidays then fall back to the previous close.

Should I convert currencies at the bid, the ask or the mid?

Use the mid for display and estimates. For a quote you will honour, start from the side your customer trades on (they sell the base currency at the bid and buy it at the ask) and add your margin explicitly.

How do I convert between two currencies that have no direct pair?

Cross them through the US dollar: multiply the rate to USD by the rate from USD, so EUR to DOP is EURUSD × USDDOP. You pay both spreads, so use a direct pair when one is listed.

Are these official exchange rates?

No. They are aggregated, indicative market rates, not a central bank fixing or any venue's official price. For contracts or accounting that name a specific fixing, use that publication.

Keep reading

Ready to integrate?

Start with the free tier, explore the docs, and connect via REST or WebSocket in minutes.