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
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.
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
}
symbolSix letters, base currency first. The price is euros quoted in dollars.bidWhat the market pays for one euro. You receive this when you sell euros.askWhat the market asks for one euro. You pay this when you buy euros.timestampUnix milliseconds, UTC: 10:49:56 on 28 September 2026.
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.
| Feature | Exchange rate data API | Broker trading API | Payment conversion API |
|---|---|---|---|
| Returns | Quotes, bars, streams | Fills and positions | A converted amount |
| Moves money | |||
| Rate you see | Market bid, ask, mid | The broker price | Provider rate plus margin |
| Typical use | Display, estimates, analytics, alerts | Trading | Checkout, payouts, treasury |
| Needs a funded account |
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.
| Rate | What it is | Use it for |
|---|---|---|
| Bid | Best price a buyer pays for the base currency | Valuing currency you would sell |
| Ask | Best price a seller accepts | Costing currency you would buy |
| Mid | (bid + ask) ÷ 2 | Display, conversion estimates, analytics |
| Daily close | Last price of a UTC day bar | Historical series, charts, reports |
| Fixing | One rate a benchmark publishes at a set time each day | Contracts or accounting rules that name it |
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.
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}") 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 USDDOPThe 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
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")EURUSD for 2026-09-26: 1.13913 (close of 2026-09-25)
EURUSD August 2026 average: 1.15918 over 21 business daysOne 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.
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.74The 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:
A WebSocket connection replaces all of that with one socket. Connect to wss://stream.tickerlayer.com/?apiKey=..., wait for the ready frame, then subscribe:
{"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
}
typeSwitch on this first: quote, trade, system or error.bidA string on the stream, unlike REST. Convert with float() or a decimal type.tsThe time field on stream frames, Unix milliseconds.
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-Limiton 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 feed | Price | REST requests | WebSocket | Use |
|---|---|---|---|---|
| Free | $0 | 3,000 a month | Trial on request | Prototyping |
| Individual | $59 a month | 250,000 a month | 1 connection, 10 symbols | Personal and research |
| Business | $549 a month | 25,000,000 a month | 10 connections, unlimited symbols | Commercial use |
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.