GuidesCRYPTO
Crypto price API from JavaScript
Prefix https://proxy.cors.dev/ to Bitfinex, MEXC, Bitstamp, Poloniex or bitFlyer URLs: free, no signup, no key, 600 requests per minute, JSON up to 1 MiB. Those exchanges send no Access-Control-Allow-Origin header, so fetch() from your page fails with a CORS error. Coinbase, Kraken, Bybit, Binance, OKX and CoinGecko do send it, so call them directly; through a shared proxy such as cors.dev, Binance answered 403 and CoinGecko mostly 429. We tested 34 endpoints from 29 crypto APIs in October 2026, and the table below has the result for each.
Which crypto APIs need a CORS proxy
CORS is set per exchange, not per coin. The browser hands a response to your script only when it carries an Access-Control-Allow-Origin header for your page. Bitfinex, MEXC, Bitstamp, Poloniex, bitFlyer, KuCoin and the Coinbase Advanced Trade market endpoints send none, so Chrome logs blocked by CORS policy even though the prices arrived. The https://proxy.cors.dev/ prefix fetches the URL from a server and returns it with the header. None of these public endpoints needs a key.
Most other exchanges already send the header, so call them directly. Through any proxy, an API sees shared server addresses instead of your visitors' own and counts that traffic together; the table shows which ones then answer 403 or 429.
Crypto APIs tested from the browser
Each endpoint was called directly with an Origin header, the way a page on another site calls it, and three times through cors.dev. Sizes are decoded bytes for one bitcoin quote, or the whole response where the endpoint returns more.
| API | Endpoint tested | Size | Direct from a page | Through cors.dev |
|---|---|---|---|---|
| Bitfinex v2 | api-pub.bitfinex.com/v2/tickers | 107 B | Blocked, no header | 200 |
| Bitfinex v1 | api.bitfinex.com/v1/pubticker/btcusd | 162 B | Blocked, no header | 200 |
| MEXC spot | api.mexc.com/api/v3/ticker/price | 39 B | Blocked, no header | 200 |
| MEXC futures | contract.mexc.com/api/v1/contract/ticker | 581 B | Blocked, no header | 200, one 403 in 3 calls |
| Bitstamp | www.bitstamp.net/api/v2/ticker/btcusd/ | 254 B | Blocked, no header | 200 |
| Poloniex | api.poloniex.com/markets/BTC_USDT/price | 103 B | Blocked, no header | 200 |
| bitFlyer | api.bitflyer.com/v1/ticker | 368 B | Blocked, no header | 200 |
| Coinbase Advanced Trade | api.coinbase.com/api/v3/brokerage/market/products/BTC-USD | 1.3 KB | Blocked, no header | 200 |
| KuCoin | api.kucoin.com/api/v1/market/stats | 396 B | Blocked, no header | 200, then 429 on bursts |
| Coinbase | api.coinbase.com/v2/prices/BTC-USD/spot | 61 B | Works, sends * | Not needed |
| Coinbase Exchange | api.exchange.coinbase.com/products/BTC-USD/ticker | 186 B | Works, sends * | Not needed |
| Kraken | api.kraken.com/0/public/Ticker | 309 B | Works, echoes origin | Not needed |
| Bybit | api.bybit.com/v5/market/tickers | 423 B | Works, echoes origin | Not needed |
| Gemini | api.gemini.com/v2/ticker/btcusd | 394 B | Works, echoes origin | Not needed |
| Crypto.com | api.crypto.com/exchange/v1/public/get-tickers | 372 B | Works, echoes origin | Not needed |
| HTX | api.htx.com/market/detail/merged | 303 B | Works, echoes origin | Not needed |
| Deribit | www.deribit.com/api/v2/public/ticker | 705 B | Works, echoes origin | Not needed |
| Gate | api.gateio.ws/api/v4/spot/tickers | 272 B | Works, sends * | Not needed |
| Upbit | api.upbit.com/v1/ticker | 833 B | Works, sends * | Not needed |
| Binance.US | api.binance.us/api/v3/ticker/price | 45 B | Works, sends * | Not needed |
| Coinlore | api.coinlore.net/api/ticker/ | 359 B | Works, sends * | Not needed |
| blockchain.info | blockchain.info/ticker | 2.8 KB | Works, sends * | Not needed |
| mempool.space | mempool.space/api/v1/prices | 108 B | Works, sends * | Not needed |
| Binance | api.binance.com/api/v3/ticker/price | 45 B | Works, sends * | 403, once 451 |
| Binance market data | data-api.binance.vision/api/v3/ticker/price | 45 B | Works, sends * | 403 |
| OKX | www.okx.com/api/v5/market/ticker | 364 B | Works, echoes origin | 429 on 1 of 3 calls |
| CoinGecko | api.coingecko.com/api/v3/simple/price | 52 B | Works, sends * | 429 on 2 of 3 calls |
| Bitget | api.bitget.com/api/v2/spot/market/tickers | 423 B | Works, sends * | 403 cloudflare block |
| Bitvavo | api.bitvavo.com/v2/ticker/price | 36 B | Works, sends * | 429, IP banned |
| CoinPaprika | api.coinpaprika.com/v1/tickers/btc-bitcoin | 821 B | Works, sends * | 402 payment required |
| DexScreener | api.dexscreener.com/latest/dex/search | 25 KB | Works, sends * | 429 Cloudflare page |
| CoinMarketCap | pro-api.coinmarketcap.com/v1/cryptocurrency/listings/latest | - | 401, key required | Same 401 |
| CryptoCompare | min-api.cryptocompare.com/data/price | - | 401, key required | Same 401 |
| CoinCap | rest.coincap.io/v3/assets/bitcoin | - | 401, key required | Same 401 |
CryptoCompare's min-api and CoinDesk's data API now return 401 without a key, as do CoinMarketCap and CoinCap v3; CoinCap's old api.coincap.io host no longer resolves. A key in browser code is readable by anyone who opens DevTools, so keyed APIs belong behind your own server or Worker.
Bitfinex API: tickers and the array format
https://api-pub.bitfinex.com/v2/tickers?symbols=tBTCUSD,tETHUSD returns one array per symbol instead of an object. Trading pairs start with t, and each row has 12 values: SYMBOL, BID, BID_SIZE, ASK, ASK_SIZE, DAILY_CHANGE, DAILY_CHANGE_RELATIVE, LAST_PRICE, VOLUME, HIGH, LOW and FIRST_TRADE, a millisecond timestamp. DAILY_CHANGE_RELATIVE is a fraction, so 0.0049 means up 0.49% in 24 hours.
// Markup: <ul id="tickers"></ul>
const FIELDS = [
'symbol', 'bid', 'bidSize', 'ask', 'askSize', 'dailyChange',
'dailyChangeRelative', 'lastPrice', 'volume', 'high', 'low', 'firstTrade',
];
async function bitfinexTickers(symbols) {
const url = 'https://api-pub.bitfinex.com/v2/tickers?symbols=' + symbols.join(',');
const response = await fetch('https://proxy.cors.dev/' + url, { credentials: 'omit' });
if (!response.ok) {
throw new Error(`Bitfinex request failed: ${response.headers.get('X-Cors-Error') ?? response.status}`);
}
const rows = await response.json();
return rows.map((row) => Object.fromEntries(FIELDS.map((field, index) => [field, row[index]])));
}
const list = document.querySelector('#tickers');
async function refresh() {
if (document.hidden) return;
try {
const tickers = await bitfinexTickers(['tBTCUSD', 'tETHUSD', 'tSOLUSD']);
list.replaceChildren(
...tickers.map((ticker) => {
const item = document.createElement('li');
const percent = (ticker.dailyChangeRelative * 100).toFixed(2);
item.textContent = `${ticker.symbol.slice(1)} ${ticker.lastPrice} (${percent}% in 24h)`;
return item;
}),
);
} catch (error) {
list.textContent = error.message;
}
}
refresh();
setInterval(refresh, 10_000);Three pairs are 342 B, and symbols=ALL returns every ticker in 41.7 KB. An unknown symbol returns 200 with an empty array, not an error. Funding symbols such as fUSD return 18 values, /v2/ticker/tBTCUSD returns 11 without the symbol, and candles from /v2/candles/trade:1h:tBTCUSD/hist come newest first as MTS, OPEN, CLOSE, HIGH, LOW and VOLUME. The legacy https://api.bitfinex.com/v1/pubticker/btcusd returns named fields such as last_price (162 B).
MEXC API: prices, 24-hour stats and candles
MEXC's spot API at https://api.mexc.com/api/v3 copies Binance's paths and sends no CORS header. Public market data needs no key, and errors come back as JSON with msg and code.
// Markup: <p id="mexc"></p>
async function mexc(path) {
const response = await fetch('https://proxy.cors.dev/https://api.mexc.com/api/v3' + path, {
credentials: 'omit',
});
const body = await response.json().catch(() => null);
if (!response.ok) {
const reason = body?.msg ?? response.headers.get('X-Cors-Error') ?? response.status;
throw new Error(`MEXC request failed: ${reason}`);
}
return body;
}
async function show() {
const ticker = await mexc('/ticker/24hr?symbol=BTCUSDT');
const change = (Number(ticker.priceChangePercent) * 100).toFixed(2);
document.querySelector('#mexc').textContent = `BTC/USDT ${ticker.lastPrice} (${change}% in 24h)`;
const candles = await mexc('/klines?symbol=BTCUSDT&interval=60m&limit=24');
console.table(
candles.map(([openTime, open, high, low, close, volume]) => ({
hour: new Date(openTime).toISOString().slice(11, 16),
open: Number(open),
high: Number(high),
low: Number(low),
close: Number(close),
volume: Number(volume),
})),
);
}
show().catch((error) => {
document.querySelector('#mexc').textContent = error.message;
});Two differences from Binance break copied code. priceChangePercent is a fraction, so 0.0047 means up 0.47%, where Binance returns 0.411 for up 0.41%. Candle intervals are 1m, 5m, 15m, 30m, 60m, 4h, 8h, 1d, 1W and 1M; 1h returns 400 Invalid interval. Kline rows have 8 values: open time, open, high, low, close, volume, close time and quote volume.
One price is 39 B, a 24-hour ticker 397 B and 24 hourly candles 2.4 KB. Without symbol you get every pair: 716 KB of 24-hour tickers, the largest response on this page and still under the 1 MiB free limit. Futures on contract.mexc.com also need the proxy and answered 403 to 1 of 3 calls. Orders and balances need a signature made with your secret key, and the free tier refuses a signature parameter; sign those requests on a server.
Coinbase, Kraken and CoinGecko: call them directly
These send Access-Control-Allow-Origin, so plain fetch() works from any page with no proxy. Coinbase's spot price is 61 B, and the price is data.amount, a string.
// No proxy: api.coinbase.com sends Access-Control-Allow-Origin: *
// Top-level await: run it in a module script or the DevTools console
async function spotPrice(pair) {
const response = await fetch(`https://api.coinbase.com/v2/prices/${pair}/spot`, {
credentials: 'omit',
});
if (!response.ok) throw new Error(`Coinbase request failed: ${response.status}`);
const { data } = await response.json();
return Number(data.amount);
}
const pairs = ['BTC-USD', 'ETH-USD', 'SOL-USD'];
const prices = await Promise.all(pairs.map(spotPrice));
console.table(Object.fromEntries(pairs.map((pair, index) => [pair, prices[index]])));Kraken calls bitcoin XBT: https://api.kraken.com/0/public/Ticker?pair=XBTUSD (309 B) returns the pair under the key XXBTZUSD, with the last price in c[0], the best ask in a[0] and the best bid in b[0], all strings. CoinGecko's keyless /api/v3/simple/price?ids=bitcoin,ethereum&vs_currencies=usd (52 B) is the usual free aggregator; call it from the page, because through cors.dev it answered 429 to 2 of 3 calls.
Live prices over WebSocket, no proxy
CORS does not apply to WebSockets: the browser sends an Origin header with the handshake and each server decides. Every feed we opened from a page on https://example.org accepted the connection and streamed prices: Bitfinex wss://api-pub.bitfinex.com/ws/2, Kraken wss://ws.kraken.com/v2, Coinbase wss://ws-feed.exchange.coinbase.com and wss://advanced-trade-ws.coinbase.com, Binance wss://stream.binance.com:9443/ws/btcusdt@ticker and MEXC wss://wbs-api.mexc.com/ws.
// Markup: <p id="live-price"></p>
const output = document.querySelector('#live-price');
const socket = new WebSocket('wss://api-pub.bitfinex.com/ws/2');
socket.addEventListener('open', () => {
socket.send(JSON.stringify({ event: 'subscribe', channel: 'ticker', symbol: 'tBTCUSD' }));
});
socket.addEventListener('message', (event) => {
const message = JSON.parse(event.data);
// Events are objects and heartbeats are [chanId, 'hb'], so we only read ticker arrays
if (!Array.isArray(message) || !Array.isArray(message[1])) return;
const [bid, , ask, , , dailyChangeRelative, lastPrice] = message[1];
const percent = (dailyChangeRelative * 100).toFixed(2);
output.textContent = `BTC/USD ${lastPrice}, bid ${bid}, ask ${ask}, ${percent}% in 24h`;
});
socket.addEventListener('close', () => {
output.textContent = 'Connection closed';
});A Bitfinex ticker message is the channel id plus the single-pair row of 11 values, BID through FIRST_TRADE. Bitfinex allows 20 connections per minute on the public host and 25 channels per connection. MEXC sends binary protobuf frames, which need its schema to decode. cors.dev does not relay WebSockets: a wss:// target returns 400.
Polling and limits
- Polling: REST prices only change when you ask again. One call for all your symbols every 10 seconds is 6 requests per minute per visitor, far below the free tier limit of 600 per minute per client IP. Skip calls while
document.hiddenis true. - Exchange limits: Bitfinex allows 30 requests per minute on /v2/tickers and blocks an IP for 60 seconds after that, so request every symbol in one call. KuCoin answered 9 of 10 rapid calls through the proxy with 429.
- Size: single-pair tickers in the table are 36 B to 1.3 KB. A response over 1 MiB fails with 502 and
X-Cors-Error: response_too_large. - Free tier rules: GET and HEAD only, no daily or monthly quota, 10 seconds for the exchange to answer, and only
Accept,If-None-MatchandIf-Modified-Sinceare forwarded. Pro adds POST, custom headers and 6 MiB responses for $5 per month.
Good questions.
Does the MEXC API support CORS?
No. api.mexc.com sends no Access-Control-Allow-Origin header, so fetch() from a web page fails. Prefix public URLs such as /api/v3/ticker/price?symbol=BTCUSDT with https://proxy.cors.dev/. Signed order and balance endpoints need your secret key and belong on a server.
Does the Bitfinex API support CORS?
Not on REST. Neither api-pub.bitfinex.com nor api.bitfinex.com sends a CORS header, so prefix the URL with https://proxy.cors.dev/. The WebSocket feed at wss://api-pub.bitfinex.com/ws/2 works without a proxy.
What do the numbers in a Bitfinex ticker mean?
A /v2/tickers row is SYMBOL, BID, BID_SIZE, ASK, ASK_SIZE, DAILY_CHANGE, DAILY_CHANGE_RELATIVE, LAST_PRICE, VOLUME, HIGH, LOW and FIRST_TRADE. DAILY_CHANGE_RELATIVE is a fraction; multiply it by 100 for a percentage.
Is there a free crypto price API with no API key?
Yes. Coinbase, Kraken, Binance, Bybit and CoinGecko serve public prices without a key and send CORS headers, so plain fetch() works. Bitfinex, MEXC, Bitstamp and Poloniex are keyless too but need a CORS proxy. CoinMarketCap, CryptoCompare and CoinCap return 401 without a key.
Why does the CoinGecko API return 429?
Its keyless tier is rate limited, and a shared proxy puts many sites' calls on the same server addresses: it answered 429 to 2 of 3 calls through cors.dev. CoinGecko sends Access-Control-Allow-Origin: *, so call it directly from the page.
Why does the Binance API return 403 through a proxy?
Binance's CloudFront front end refused the proxy's server addresses: api.binance.com answered 403, once 451 for a restricted location, and data-api.binance.vision 403. Both send Access-Control-Allow-Origin: *, so call them directly.
Can I get live crypto prices over WebSocket without a proxy?
Yes. CORS does not apply to WebSockets, and the Bitfinex, Kraken, Coinbase, Binance and MEXC feeds all accepted a connection from a page on another origin in our test. cors.dev does not relay WebSockets.