biquote
Home / Docs / Guides

Get Free Forex Data in Python — No API Key Required

Most "free" forex APIs cap you at a few hundred requests a day and make you register for a key first. biquote is different: every endpoint is anonymous, and the limit is 15,000 requests per minute. Here is everything you need.

Shortcut: there's an official client — pip install biquote — that wraps everything below (mid-as-price, OHLC envelope, liveOnly). The raw-requests path stays here so you can see exactly what's on the wire.

1. A live quote in one line

import requests

tick = requests.get("https://biquote.io/api/EURUSD").json()
print(tick["mid"], tick["dayDiffPercent"])   # 1.15884 0.51

Or with the client:

from biquote import Biquote

bq = Biquote()
print(bq.tick("EURUSD")["mid"])
Use mid as the price. last and volume are always 0 on FX/CFD feeds — there is no central exchange tape for forex.

2. Many symbols in one request

Polling one symbol per call wastes your quota. The batch endpoint takes up to 100 symbols (repeat the parameter — the comma form is not supported):

r = requests.get(
    "https://biquote.io/api/latest",
    params=[("symbols", s) for s in ("EURUSD", "XAUUSD", "BTCUSD", "USDTRY")],
).json()
for sym, t in r.items():
    print(f'{sym:8} {t["mid"]:>12}  {t["dayDiffPercent"]:+.2f}%')

3. OHLC candles into pandas

import pandas as pd

env = requests.get(
    "https://biquote.io/api/XAUUSD/ohlc",
    params={"interval": "1h", "limit": 500},
).json()
df = pd.DataFrame(env["bars"])            # envelope: {symbol, interval, bars}
df["openTime"] = pd.to_datetime(df["openTime"])
df = df.set_index("openTime").sort_index()  # bars arrive newest-first
print(df[["open", "high", "low", "close"]].tail())

Intervals: 1m 5m 15m 30m 1h 4h 1d, up to 1,000 bars per call.

4. Streaming instead of polling

For continuous updates, skip HTTP entirely — the SignalR hub pushes every tick with no rate limit:

pip install pysignalr
import asyncio
from pysignalr.client import SignalRClient

async def main():
    client = SignalRClient("https://biquote.io/hubs/tick")
    client.on("ReceiveTick", lambda t: print(t[0]["symbol"], t[0]["mid"]))
    client.on_open(lambda: client.send("Subscribe", [["EURUSD", "XAUUSD"]]))
    await client.run()

asyncio.run(main())

Which symbols are live?

The catalogue lists ~1,700 instruments but only ~280 quote at any moment. Always filter:

live = requests.get("https://biquote.io/api/symbols",
                    params={"quotedWithinDays": 7}).json()
print(len(live), "instruments")

quotedWithinDays rather than liveOnly: the latter means "quoting this second", so at a weekend it returns almost nothing and any list built on it comes back empty.

Rate limits & errors

LimitValue
Anonymous quota15,000 requests / minute / IP
Over the limitHTTP 429 with Retry-After: 60
Unknown symbolHTTP 404 {"message": "..."}
Market closedHTTP 200 with the last price and "marketState": "closed"

That last row is the one that catches people. Forex stops quoting from Friday night to Sunday night, and a closed market is not an error — you get the last known price plus marketState, stale and quoteAgeSeconds. Show it as a last price rather than a live one, and hide the day change, which belongs to a session that has already ended:

tick = requests.get("https://biquote.io/api/EURUSD").json()

if tick["marketState"] == "closed":
    print(f"{tick['mid']}  (market closed, {tick['quoteAgeSeconds'] // 3600}h ago)")
else:
    print(f"{tick['mid']}  {tick['dayDiffPercent']:+.2f}%")

Writing a trading client instead? Pass allowStale=false and you get a 404 the moment a quote is more than five minutes old — the right behaviour when acting on a stale price is worse than acting on none.

Read the full API reference →