biquote API Reference
Real-time market data API for Forex, Crypto, Metals and Index CFDs — REST endpoints and live WebSocket streaming via SignalR.
Base URL & Authentication
All REST endpoints are prefixed with /api. The API is publicly accessible — no API key, no signup, no authentication for any read endpoint.
X-Api-Key header.Service status: GET /health returns feed and collector health as JSON.
Rate Limits
/hubs/tick pushes
every tick to you over one connection, and it does
not count against the rate limit at all. It is cheaper for
you and for us than any polling loop. If you must poll, poll
/api/latest with many symbols at once —
never one symbol per request.Anonymous callers get 15,000 requests per minute per IP in a fixed one-minute window. Exceeding it returns 429 Too Many Requests with these headers:
| Header | Meaning |
|---|---|
Retry-After | Seconds until the window resets (60) |
X-RateLimit-Limit | Your per-minute request quota |
X-RateLimit-Remaining | Requests left in the current window (0 on a 429) |
X-RateLimit-Reset | Seconds until the counter resets |
Staying under the limit is easy:
- Batch — fetch many symbols in one request with
/api/latest:?symbols=EURUSD&symbols=XAUUSD&symbols=BTCUSD(repeat thesymbolsparameter — the comma form is not supported). - Stream — the SignalR hub at
/hubs/tickpushes ticks over one connection with no polling and no rate limit. - Back off — on a 429, wait the
Retry-Afterseconds before retrying.
Error Handling
The API uses standard HTTP status codes:
| Code | Meaning |
|---|---|
| 200 | Success |
| 400 | Bad Request — missing or invalid parameters |
| 404 | Not Found — unknown symbol, or one that has not quoted in over 7 days. Not what a closed market returns |
| 429 | Too Many Requests — see Rate Limits; honor Retry-After |
| 500 | Server Error |
| 503 | Market data store unreachable — an outage on our side, not a closed market. Retry after Retry-After |
{
"message": "No tick data available for 'INVALIDXYZ'"
}
Rate-limit rejections and calendar validation errors use an error field instead of message:
{
"error": "rate_limited",
"message": "Too many requests. Retry after 60 seconds.",
"hint": "Use GET /api/latest?symbols=... to batch, or stream via SignalR at /hubs/tick.",
"docs": "https://biquote.io/docs/"
}
Ticks
Access real-time and historical tick data (bid, ask, mid, spread, day range) for any active symbol.
marketState: "closed" — not an error. Render it as
"market closed — last price", and hide the day-change percentage, which belongs
to a session that has ended.
Building a trading client? Pass
allowStale=false for the strict behaviour:
404 as soon as a quote is over five minutes old.Returns the most recent tick data for the specified symbol.
| Name | Type | Required | Description |
|---|---|---|---|
| symbol | string | required | Symbol name (e.g. EURUSD, XAUUSD, BTCUSD) |
| allowStale | boolean | optional | Default true — a closed market returns its last known price.
Pass false to get 404 instead once a quote is over
five minutes old. |
curl https://biquote.io/api/EURUSD
const res = await fetch('https://biquote.io/api/EURUSD');
const tick = await res.json();
console.log(tick.bid, tick.ask);
import requests
tick = requests.get('https://biquote.io/api/EURUSD').json()
print(tick['bid'], tick['ask'])
{
"symbol": "EURUSD",
"description": "Euro vs US Dollar",
"bid": 1.08542,
"ask": 1.08548,
"mid": 1.08545,
"spread": 0.00006,
"last": 0.0,
"volume": 0,
"high": 1.08800,
"low": 1.08530,
"direction": "FLAT",
"dayDiffPercent": -0.09,
"timestamp": "2026-02-24T10:30:00Z",
"time": "2026.02.24 10:30:00",
"source": "MetaTrader 5 (Broker 1)",
"marketState": "open",
"stale": false,
"quoteAgeSeconds": 0,
"lastQuoteAt": "2026-02-24T10:30:00Z"
}
last and volume are always 0 for FX/CFD sources — use mid for a single price.
marketState, stale and quoteAgeSeconds are on
every response, live or not — so freshness is never something you have
to infer from a field being absent. The same verdict is on the
X-Quote-State and X-Quote-Age-Seconds headers.
time is MetaTrader's native format and is not reliably
parseable by new Date() across browsers — parse
timestamp.Returns the latest tick for multiple symbols in a single request.
| Name | Type | Required | Description |
|---|---|---|---|
| symbols | string[] | required | One or more symbol names. Repeat parameter for multiple: ?symbols=EURUSD&symbols=XAUUSD |
curl "https://biquote.io/api/latest?symbols=EURUSD&symbols=XAUUSD&symbols=BTCUSD"
Returns historical tick records for a symbol, newest first.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| symbol | string | required | — | Symbol name |
| count | integer | optional | 100 | Number of ticks to return (1–1000) |
curl "https://biquote.io/api/EURUSD/history?count=50"
Returns all symbols that currently have live tick data, including metadata.
curl https://biquote.io/api/active
[
{
"name": "EURUSD",
"description": "Euro vs US Dollar",
"type": "Forex",
"exchange": "Forex",
"source": "MT5"
},
{
"name": "XAUUSD",
"description": "Gold vs US Dollar",
"type": "Commodity",
"exchange": "Forex",
"source": "MT5"
}
]
Symbols
Query symbol metadata — name, description, type, exchange, and data source.
| Name | Type | Required | Description |
|---|---|---|---|
| source | string | optional | MT5 or MATRIKS |
| type | string | optional | Forex, Stock, Crypto, Index, Commodity |
| exchange | string | optional | Filter by exchange (e.g. Forex) |
| activeOnly | boolean | optional | Only symbols the broker lists as active (default false) |
| liveOnly | boolean | optional | Only symbols with live data right now (default false) |
isActive means "the broker lists it" and is true for nearly all of them.
For a picker, a nav menu or any cached list, pass
quotedWithinDays=7.
liveOnly=true means "quoting this second" and so returns almost
nothing at a weekend — a list built on it empties out at exactly the times people
browse.# Symbols with live data right now
curl "https://biquote.io/api/symbols?liveOnly=true"
# All MT5 symbols
curl "https://biquote.io/api/symbols?source=MT5"
| Name | Type | Required | Description |
|---|---|---|---|
| q | string | required | Search query — matches name and description |
| liveOnly | boolean | optional | Only symbols with live data right now (default false) |
| limit | integer | optional | Max results (default 25, cap 200) |
curl "https://biquote.io/api/symbols/search?q=gold&liveOnly=true"
curl https://biquote.io/api/symbols/EURUSD
Market Statistics
Top gainers and losers across all asset classes with configurable time periods.
| Name | Type | Default | Description |
|---|---|---|---|
| type | string | — | Forex, Stock, Crypto, etc. |
| exchange | string | — | Filter by exchange |
| limit | integer | 10 | Number of results (1–100) |
| period | string | 1D | Accepted for compatibility but currently ignored — ranking is always by change since the daily open |
# Top 5 forex gainers today
curl "https://biquote.io/api/market/gainers?type=Forex&limit=5&period=1D"
{
"period": "1D",
"items": [
{
"symbol": "XAUUSD",
"description": "Gold vs US Dollar",
"lastPrice": 4396.09,
"changePercent": 4.25,
"changeAmount": 179.20,
"volume": 0
}
]
}
Same parameters as /api/market/gainers — returns symbols sorted by largest percentage decline.
curl "https://biquote.io/api/market/losers?type=Crypto&limit=10&period=1H"
Same type/exchange/limit filters as /api/market/gainers (no period — ranking is over today's session). Returns the symbols that moved the most today, ranked by relative intraday range (high−low over mid). Traded volume is not available on this feed, so ranking is movement-based.
curl "https://biquote.io/api/market/most-active?limit=10"
Overall market snapshot: counts of gaining, losing and unchanged instruments, per asset class.
curl "https://biquote.io/api/market/summary"
News
Financial news feed aggregated from multiple sources. Filter by symbol, language and country.
Returns market and finance related news, aggregated from Google News plus recent well-scored Hacker News stories (about a third of the slots at most), interleaved newest-first. Optionally filter by stock symbol — the Hacker News search follows the filter. Items are distinguishable by their publisher field.
| Name | Type | Default | Description |
|---|---|---|---|
| symbol | string | — | Optional symbol to filter news (e.g. AAPL, XAUUSD) |
| language | string | en | Language code (en, tr, de…) |
| country | string | US | Country code (US, TR, GB…) |
| maxResults | integer | 10 | Number of articles (1–50) |
# All market news
curl "https://biquote.io/api/news/market"
# Filtered by symbol
curl "https://biquote.io/api/news/market?symbol=AAPL&maxResults=5"
const res = await fetch('https://biquote.io/api/news/market?language=tr&country=TR');
const news = await res.json();
news.forEach(a => console.log(a.title, a.publishedDate));
import requests
news = requests.get(
'https://biquote.io/api/news/market',
params={'language': 'tr', 'country': 'TR', 'maxResults': 10}
).json()
for a in news:
print(a['title'], a['publishedDate'])
[
{
"title": "Markets rally as Fed signals rate pause",
"description": "Wall Street surged on Wednesday after...",
"url": "https://example.com/article",
"publisher": "Reuters",
"publishedDate": "2026-02-24T09:15:00Z",
"imageUrl": null,
"category": null,
"language": "en",
"country": "US"
}
]
The current Hacker News front page, in rank order — or a relevance-ranked story search when q is given. Same article shape as the other news feeds; description carries the points and comment counts, and Ask HN posts link to their HN discussion since they have no external URL.
| Name | Type | Default | Description |
|---|---|---|---|
| q | string | — | Optional search keyword (e.g. bitcoin, trading) |
| maxResults | integer | 10 | Number of stories (1–50) |
# Front page
curl "https://biquote.io/api/news/hn"
# Story search
curl "https://biquote.io/api/news/hn?q=bitcoin&maxResults=5"
[
{
"title": "Show HN: A real-time market data API",
"description": "342 points · 128 comments",
"url": "https://example.com/story",
"publisher": "Hacker News",
"publishedDate": "2026-08-19T06:30:00Z",
"imageUrl": null,
"category": null,
"language": "en",
"country": "US"
}
]
Economic Calendar
Scheduled macroeconomic releases with their actual, forecast and previous readings, across 54 countries. Two feeds sit behind it and their coverage deliberately does not overlap: the MetaTrader 5 economic calendar supplies the twenty-two majors, and a second feed supplies what it omits — Turkey, central and eastern Europe, the smaller euro-area members, Asian emerging markets and the Gulf. Both are normalised to the same shape, so nothing you write needs to know which one a row came from. The store holds a rolling window of roughly 90 days back and 90 days forward.
Call GET /api/calendar/countries for the exact list. A calendar this broad is also a noisy one — filter on importance unless you genuinely want every treasury-bill auction.
Returns events ordered oldest-first as a bare JSON array.
| Name | Type | Default | Description |
|---|---|---|---|
| from | ISO 8601 | yesterday | Start of range, UTC |
| to | ISO 8601 | +7 days | End of range, UTC |
| countries | string | — | Comma-separated ISO alpha-2 (US,EU,GB,JP,TR). Omit for all |
| importance | string | — | Minimum importance: low, medium, high |
| type | string | — | event, indicator or holiday |
| limit | integer | 200 | Maximum rows (1–500) |
# This week, everything
curl "https://biquote.io/api/calendar"
# High-impact US, EU and Turkish releases only
curl "https://biquote.io/api/calendar?countries=US,EU,TR&importance=high"
const res = await fetch('https://biquote.io/api/calendar?importance=high&limit=50');
const events = await res.json();
for (const e of events) {
// actual is null until the figure is published — never treat it as 0
const status = e.actual === null ? 'upcoming' : `actual ${e.actual}`;
console.log(e.time, e.countryCode, e.name, status);
}
import requests
events = requests.get(
'https://biquote.io/api/calendar',
params={'countries': 'US,TR', 'importance': 'high', 'limit': 50}
).json()
for e in events:
print(e['time'], e['countryCode'], e['name'], e['actual'], e['forecast'])
[
{
"id": "mql5:900001",
"eventId": "mql5:840",
"time": "2026-08-07T12:30:00Z",
"period": "2026-07-01T00:00:00Z",
"countryCode": "US",
"currency": "USD",
"name": "Nonfarm Payrolls",
"importance": "high",
"type": "indicator",
"sector": "jobs",
"unit": "job",
"multiplier": "thousands",
"digits": 0,
"actual": 152000,
"forecast": 145000,
"previous": 147000,
"revisedPrevious": 149000,
"revision": 1,
"timeMode": "exact",
"sourceUrl": "https://www.bls.gov/",
"source": "mql5"
}
]
| Field | What it means |
|---|---|
| actual / forecast / previous | null means not published, not zero. A forthcoming release has no actual; most releases carry no forecast; a holiday carries none of them. |
| multiplier | Scale to display, not to multiply out. Payrolls arrives as 152000 with "thousands" — show 152,000K. |
| unit | percent, currency, job, barrel… how to suffix the number. |
| timeMode | exact, date, notime or tentative. Anything but exact means the clock component is a placeholder. |
| revisedPrevious | Set when the statistics office restated the prior figure. Compare actual against forecast, not against previous. |
The next scheduled releases, soonest first, over a 30-day horizon.
| Name | Type | Default | Description |
|---|---|---|---|
| limit | integer | 20 | Maximum rows (1–500) |
| countries | string | — | Comma-separated ISO alpha-2 |
| importance | string | — | Minimum importance |
curl "https://biquote.io/api/calendar/upcoming?limit=10&importance=high"
Country codes present in the calendar, with names and currencies. Use these values for the countries filter.
[
{ "code": "EU", "name": "EU", "currency": "EUR" },
{ "code": "TR", "name": "Türkiye", "currency": "TRY" },
{ "code": "US", "name": "US", "currency": "USD" }
]
Past releases of one recurring series, newest first, so a figure can be read against its own history rather than a single previous print. eventId is the eventId field from any event. Returns 404 when the series is unknown.
| Name | Type | Default | Description |
|---|---|---|---|
| limit | integer | 24 | Maximum rows (1–500) |
curl "https://biquote.io/api/calendar/mql5:840/history?limit=12"
OHLC / Candlestick
Retrieve OHLCV candlestick bars for any symbol. Data is sourced from Yahoo Finance (bootstrap), real-time tick aggregation, and MT5 historical feeds. Supports M1 through D1 timeframes with up to 2000 bars per series.
Returns OHLCV bars for the given symbol and timeframe. The most recent (open) bar is prepended with isOpen: true.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| symbol | string | required | — | Symbol name (e.g. EURUSD, BTCUSD, XAUUSD) |
| interval | string | optional | 1h |
Timeframe: 1m 5m 15m 30m 1h 4h 1d |
| limit | integer | optional | 100 |
Number of bars to return (1–1000) |
| from | ISO 8601 | optional | — | Start of range, e.g. 2026-01-01T00:00:00Z |
| to | ISO 8601 | optional | — | End of range |
# Latest 100 hourly bars
curl "https://biquote.io/api/EURUSD/ohlc?interval=1h&limit=100"
# Daily bars for a date range
curl "https://biquote.io/api/BTCUSD/ohlc?interval=1d&from=2026-01-01T00:00:00Z"
const res = await fetch('https://biquote.io/api/EURUSD/ohlc?interval=1h&limit=200');
const data = await res.json();
// data.bars → array of { openTime, open, high, low, close, volume, tickVolume, isOpen }
const closedBars = data.bars.filter(b => !b.isOpen);
console.log(`${data.symbol} ${data.interval}: ${closedBars.length} bars`);
import requests
import pandas as pd
r = requests.get('https://biquote.io/api/EURUSD/ohlc', params={'interval': '1h', 'limit': 500})
data = r.json()
df = pd.DataFrame(data['bars'])
df['openTime'] = pd.to_datetime(df['openTime'])
df = df.set_index('openTime')
print(df[['open','high','low','close']].tail())
{
"symbol": "EURUSD",
"interval": "1h",
"bars": [
{
"openTime": "2026-02-24T13:00:00Z",
"open": 1.08521,
"high": 1.08574,
"low": 1.08498,
"close": 1.08542,
"volume": 0,
"tickVolume": 340,
"isOpen": true
},
{
"openTime": "2026-02-24T12:00:00Z",
"open": 1.08480,
"high": 1.08530,
"low": 1.08440,
"close": 1.08521,
"volume": 0,
"tickVolume": 412,
"isOpen": false
}
]
}
isOpen: true) reflects live tick aggregation and is updated in real time.SignalR Hub — Real-time Streaming
Connect to the SignalR hub for live tick data. The hub pushes updates as market data arrives — no polling required.
npm install @microsoft/signalr or use the CDN: https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/8.0.0/signalr.min.jsClient → Server Methods
Subscribe to one or more symbols. You'll receive a ReceiveTick event for each update.
| Parameter | Type | Description |
|---|---|---|
| symbols | string[] | Array of symbol names |
await connection.invoke('Subscribe', ['EURUSD', 'XAUUSD', 'BTCUSD']);
await connection.invoke('Unsubscribe', ['EURUSD']);
const tick = await connection.invoke('GetLatestTick', 'EURUSD');
console.log(tick.bid, tick.ask);
const symbols = await connection.invoke('GetSubscriptions');
console.log(symbols); // ['EURUSD', 'XAUUSD']
Server → Client Events
Fired whenever a subscribed symbol receives new market data.
connection.on('ReceiveTick', (tick) => {
console.log(`${tick.symbol}: bid=${tick.bid} ask=${tick.ask}`);
});
Quick Start
Full example — connect, subscribe, and receive live prices in the browser.
import * as signalR from '@microsoft/signalr';
const connection = new signalR.HubConnectionBuilder()
.withUrl('https://biquote.io/hubs/tick')
.withAutomaticReconnect()
.build();
// Listen for live ticks
connection.on('ReceiveTick', (tick) => {
console.log(`[${tick.symbol}] bid: ${tick.bid} ask: ${tick.ask}`);
});
// Connect and subscribe
await connection.start();
await connection.invoke('Subscribe', ['EURUSD', 'XAUUSD', 'BTCUSD']);
import requests
# REST: Get latest tick
tick = requests.get('https://biquote.io/api/EURUSD').json()
print(f"EURUSD bid={tick['bid']} ask={tick['ask']}")
# REST: Top gainers today
gainers = requests.get(
'https://biquote.io/api/market/gainers',
params={'limit': 5, 'period': '1D'}
).json()
for item in gainers['items']:
print(f"{item['symbol']}: +{item['changePercent']:.2f}%")
// Install: dotnet add package Microsoft.AspNetCore.SignalR.Client
using Microsoft.AspNetCore.SignalR.Client;
var connection = new HubConnectionBuilder()
.WithUrl("https://biquote.io/hubs/tick")
.WithAutomaticReconnect()
.Build();
connection.On<object>("ReceiveTick", tick =>
{
Console.WriteLine($"Tick received: {tick}");
});
await connection.StartAsync();
await connection.InvokeAsync("Subscribe", new[] { "EURUSD", "XAUUSD" });
import requests
import pandas as pd
# Fetch 500 hourly EURUSD candles
r = requests.get('https://biquote.io/api/EURUSD/ohlc', params={'interval': '1h', 'limit': 500})
data = r.json()
df = pd.DataFrame(data['bars'])
df['openTime'] = pd.to_datetime(df['openTime'])
df = df.set_index('openTime').sort_index()
# Drop the still-open bar
df = df[~df['isOpen']]
print(df[['open','high','low','close','tickVolume']].tail(10))
# Example: simple moving average
df['sma20'] = df['close'].rolling(20).mean()
print(df[['close','sma20']].tail(5))
Data Models
Tick
- symbolstringSymbol name (e.g. EURUSD)
- descriptionstringHuman-readable name
- bidnumberBest bid price
- asknumberBest ask price
- midnumberMidpoint of bid/ask — use this as the price
- spreadnumberask − bid
- lastnumberAlways
0for FX/CFD sources — usemid - volumenumberAlways
0for FX/CFD sources - highnumberDay's highest price
- lownumberDay's lowest price
- directionstring
UP,DOWNorFLATvs the previous tick - dayDiffPercentnumberPercent change since the daily open
- timestampstring (ISO 8601)Tick timestamp (UTC)
- sourcestringData source (e.g.
MetaTrader 5 (Broker 1))
Symbol
- namestringSymbol ticker
- descriptionstringFull name
- typestring
Forex/Stock/Crypto/Index/Commodity - exchangestringExchange name (e.g.
Forex,Crypto) - sourcestringData provider:
MT5orMATRIKS
OhlcBar
- openTimestring (ISO 8601)Bar open timestamp (UTC)
- opennumberOpening price
- highnumberHighest price in the bar
- lownumberLowest price in the bar
- closenumberClosing price (last tick if bar is open)
- volumeintegerReal volume (0 for Forex)
- tickVolumeintegerNumber of ticks in the bar
- isOpenboolean
truefor the current unfinished bar; updated on every tick