Currency API with CORS Enabled — Use Live Exchange Rates Directly in the Browser
The Currency Exchange Tool API has CORS enabled (Access-Control-Allow-Origin: *). Call live exchange rates directly from browser JavaScript — no proxy, no server, no API key.
Currency API with CORS Enabled — No Proxy, No Server, No Key
Last updated: June 28, 2026 — Read time: 6 min
Most free currency APIs have a fatal flaw for frontend developers: they don't support CORS. You can't call them from the browser. You need a backend proxy, a serverless function, or a build-time fetch. It's a headache.
The Currency Exchange Tool API is different. Every endpoint returns Access-Control-Allow-Origin: * and supports OPTIONS preflight. You can call it directly from fetch() in the browser — no proxy, no server, no API key.
What Is CORS and Why Does It Matter?
CORS (Cross-Origin Resource Sharing) is a browser security mechanism. By default, fetch() from https://yoursite.com to https://api.someother.com is blocked — unless the API explicitly allows it with CORS headers.
| API | CORS Support | Browser Call Possible? |
|---|---|---|
| Currency Exchange Tool | ✅ Access-Control-Allow-Origin: * |
Yes — directly |
| ExchangeRate-API | ❌ | No — needs backend proxy |
| Fixer.io | ❌ | No — needs backend proxy |
| Open Exchange Rates | ❌ | No — needs backend proxy |
| Frankfurter | ✅ | Yes |
If an API lacks CORS, you need one of these workarounds:
- A backend proxy (Express, FastAPI, Cloudflare Worker) — adds cost and latency
- A serverless function (Vercel, Netlify) — another thing to deploy and maintain
- JSONP (deprecated, insecure) — not recommended
With CORS enabled, none of that is necessary. Just fetch() and go.
Browser Fetch — The Simplest Example
// This works directly in the browser console — no proxy, no server
const res = await fetch(
'https://www.currencyexchangetool.com/api/v1/convert?amount=100&from=USD&to=EUR'
);
const data = await res.json();
console.log(`100 USD = ${data.result} EUR`);
Every response includes the CORS header:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Content-Type: application/json
Building a Live Rate Widget
Here is a complete embeddable currency rate widget that works with zero backend:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Live Currency Rate</title>
<style>
.rate-widget {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 16px;
max-width: 320px;
}
.rate-widget .amount { font-size: 28px; font-weight: 800; color: #111827; }
.rate-widget .rate { font-size: 13px; color: #6b7280; margin-top: 4px; }
.rate-widget .change { font-size: 12px; margin-top: 2px; }
.rate-widget .change.up { color: #059669; }
.rate-widget .change.down { color: #dc2626; }
.rate-widget .updated { font-size: 11px; color: #9ca3af; margin-top: 8px; }
</style>
</head>
<body>
<div class="rate-widget">
<div id="loading">Loading rate...</div>
<div id="content" style="display: none;">
<div class="amount" id="result"></div>
<div class="rate" id="rate"></div>
<div class="change" id="change"></div>
<div class="updated" id="updated"></div>
</div>
</div>
<script>
(async function () {
const API = 'https://www.currencyexchangetool.com/api/v1/convert';
try {
const res = await fetch(`${API}?amount=1&from=USD&to=UAH`);
const data = await res.json();
if (!data.success) throw new Error(data.error);
// Show result
document.getElementById('loading').style.display = 'none';
document.getElementById('content').style.display = 'block';
document.getElementById('result').textContent =
`1 ${data.from} = ${data.result} ${data.to}`;
document.getElementById('rate').textContent =
`Rate: ${data.rate}`;
// 24h change
const changeEl = document.getElementById('change');
if (data.changePct24h) {
const change = data.changePct24h;
changeEl.textContent =
`24h: ${change > 0 ? '+' : ''}${change.toFixed(2)}%`;
changeEl.className = `change ${change >= 0 ? 'up' : 'down'}`;
}
// When was this updated
document.getElementById('updated').textContent =
`Updated: ${new Date(data.updatedAt).toLocaleTimeString()}`;
} catch (err) {
document.getElementById('loading').textContent =
`Error: ${err.message}`;
document.getElementById('loading').style.color = '#dc2626';
}
})();
</script>
</body>
</html>
Copy this HTML file anywhere — it works. No build step, no npm install, no API key.
React Component with CORS
import { useState, useEffect } from 'react';
export default function LiveRate({ from = 'USD', to = 'EUR' }) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchRate() {
try {
// CORS-enabled — no proxy needed
const res = await fetch(
`https://www.currencyexchangetool.com/api/v1/convert?amount=1&from=${from}&to=${to}`
);
const json = await res.json();
if (!cancelled && json.success) setData(json);
if (!cancelled && !json.success) setError(json.error);
} catch (e) {
if (!cancelled) setError(e.message);
}
}
fetchRate();
const interval = setInterval(fetchRate, 60000); // refresh every 60s
return () => { cancelled = true; clearInterval(interval); };
}, [from, to]);
if (error) return <div style={{ color: '#dc2626' }}>⚠ {error}</div>;
if (!data) return <div>Loading...</div>;
const change = data.changePct24h || 0;
const changeColor = change >= 0 ? '#059669' : '#dc2626';
return (
<div style={{ fontFamily: 'system-ui', padding: 12 }}>
<div style={{ fontSize: 24, fontWeight: 800 }}>
1 {data.from} = {data.result} {data.to}
</div>
<div style={{ fontSize: 13, color: '#6b7280' }}>
Rate: {data.rate} ·
<span style={{ color: changeColor }}>
{' '}{change > 0 ? '+' : ''}{change.toFixed(2)}% 24h
</span>
</div>
</div>
);
}
How CORS Works — Technical Details
When your browser calls the API, it sends an Origin header:
GET /api/v1/convert?amount=1&from=USD&to=EUR HTTP/1.1
Host: www.currencyexchangetool.com
Origin: https://yoursite.com
The API responds with:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Allow-Origin: * means any origin can call this API. The browser sees this header, verifies it, and allows your JavaScript to read the response.
For OPTIONS preflight requests (browsers send these automatically for non-simple requests), the API returns:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, OPTIONS
No configuration needed — it just works.
Common Pitfalls (and Solutions)
"Failed to fetch" or "Network Error"
This usually means the API is blocked by a content blocker, ad blocker, or corporate firewall. Test with:
fetch('https://www.currencyexchangetool.com/api/v1/currencies')
.then(r => r.json())
.then(d => console.log('OK:', d.count, 'currencies'))
.catch(e => console.error('Blocked:', e));
Rate Limiting (429)
The browser API allows 100 req/min. If you exceed this:
- Cache responses for 60 seconds
- Check
X-RateLimit-Remainingbefore making requests - Honor the
Retry-Afterheader
Caching Stale Rates
For display purposes, cache the rate for 60 seconds in localStorage:
function getCachedRate(from, to) {
const key = `rate_${from}_${to}`;
const cached = localStorage.getItem(key);
if (cached) {
const { rate, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < 60_000) return rate;
}
return null;
}
function setCachedRate(from, to, rate) {
localStorage.setItem(
`rate_${from}_${to}`,
JSON.stringify({ rate, timestamp: Date.now() })
);
}
Why This Matters for Developers
- No backend needed — build a fully static site with live exchange rates
- No API key management — nothing to leak, rotate, or store in env vars
- Faster development — prototype directly in the browser console
- No cost — no monthly cap, no paid tier upsell
- Simpler architecture — fewer moving parts, fewer failure points
📖 Read the full API docs → 📘 JavaScript API guide → 🐍 Python API guide →