---
title: "Free Exchange Rate API Key: How to Hide It Behind a Proxy"
description: "A free exchange rate API key in frontend or mobile code is public. Build a Cloudflare Workers or Next.js proxy with caching, CORS and rate limits."
canonical: https://allratestoday.com/blog/hide-exchange-rate-api-key-proxy/
source: AllRatesToday (https://allratestoday.com/) — Markdown mirror of the page above; index of all pages at https://allratestoday.com/llms.txt
generated: 2026-11-11T15:51:36.897Z
---

You got a free exchange rate API key, pasted it into your React app, and it works. It also means the key is now public. Anyone who opens your site can copy it and spend your quota, and on a paid plan they spend your money. This guide shows why that happens and how to fix it with a small proxy: first on Cloudflare Workers, then as a Next.js route handler.

## Why a free exchange rate API key in frontend code is public

There is no way to hide a secret in code that runs on someone else's device.

- **Browser JavaScript.** The key is in your bundle, and it is in every request in the network tab. Environment variables such as `VITE_` or `NEXT_PUBLIC_` are inlined into the bundle at build time; they are not secret.
- **Query strings.** AllRatesToday's `/api/rate` endpoint accepts `?key=` for quick browser GETs. That is convenient for testing, but a key in a URL also ends up in browser history, proxy logs and referrer headers.
- **Mobile apps.** An APK or IPA can be unpacked. Obfuscation slows an attacker down for minutes, not days.

The fix is always the same: the key lives on a server you control, and the client calls that server.

## What the proxy should do

A good exchange rate proxy is small, but it does more than forward requests:

1. **Add the key** as `Authorization: Bearer ...` on the server side.
2. **Cache** responses for 60 seconds. Rates refresh every 60 seconds upstream, so a longer-lived browser does not need fresher data, and one cached answer can serve every visitor.
3. **Allowlist** currency codes and limit the number of targets, so the proxy cannot be used as a free general-purpose API.
4. **Send CORS headers** only for your own origin.
5. **Rate limit** per IP, so one script cannot drain your quota.
6. **Return safe errors.** Never echo upstream error bodies or headers that might contain account details.

## Proxy on Cloudflare Workers

Create a Worker and store the key as a secret:

```
npm create cloudflare@latest fx-proxy -- --type hello-world
cd fx-proxy
npx wrangler secret put ALLRATES_API_KEY
```

Add a rate limiting binding to `wrangler.toml`. The limit below is 30 requests per 60 seconds per key; `period` can be 10 or 60.

```
name = "fx-proxy"
main = "src/index.js"
compatibility_date = "2026-11-01"

[[ratelimits]]
name = "RATE_LIMITER"
namespace_id = "1001"
simple = { limit = 30, period = 60 }
```

Then the Worker:

```
// src/index.js
const ALLOWED = new Set(["USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF", "INR", "CNY", "SGD"]);
const ORIGIN = "https://www.example.com";
const MAX_TARGETS = 10;

function corsHeaders() {
  return {
    "Access-Control-Allow-Origin": ORIGIN,
    "Access-Control-Allow-Methods": "GET, OPTIONS",
    "Access-Control-Allow-Headers": "Content-Type",
    "Vary": "Origin",
  };
}

function json(body, status = 200) {
  return new Response(JSON.stringify(body), {
    status,
    headers: { "Content-Type": "application/json", ...corsHeaders() },
  });
}

export default {
  async fetch(request, env, ctx) {
    if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: corsHeaders() });
    if (request.method !== "GET") return json({ error: "method not allowed" }, 405);

    const url = new URL(request.url);
    if (url.pathname !== "/rates") return json({ error: "not found" }, 404);

    const source = (url.searchParams.get("source") || "USD").toUpperCase();
    const targets = [...new Set((url.searchParams.get("target") || "").toUpperCase().split(",").filter(Boolean))].sort();
    if (!ALLOWED.has(source) || targets.length === 0 || targets.length > MAX_TARGETS ||
        !targets.every((t) => ALLOWED.has(t))) {
      return json({ error: "currency not allowed" }, 400);
    }

    const ip = request.headers.get("CF-Connecting-IP") || "unknown";
    const { success } = await env.RATE_LIMITER.limit({ key: ip });
    if (!success) return json({ error: "too many requests" }, 429);

    const upstream = `https://allratestoday.com/api/v1/rates?source=${source}&target=${targets.join(",")}`;
    const cacheKey = new Request(upstream);          // the key is not part of the cache key
    const cache = caches.default;

    let cached = await cache.match(cacheKey);
    if (!cached) {
      const res = await fetch(upstream, { headers: { Authorization: `Bearer ${env.ALLRATES_API_KEY}` } });
      if (!res.ok) return json({ error: "upstream unavailable" }, 502);
      cached = new Response(await res.text(), {
        headers: { "Content-Type": "application/json", "Cache-Control": "public, max-age=60" },
      });
      ctx.waitUntil(cache.put(cacheKey, cached.clone()));
    }

    const out = new Response(cached.body, cached);
    for (const [k, v] of Object.entries(corsHeaders())) out.headers.set(k, v);
    return out;
  },
};
```

Deploy with `npx wrangler deploy` and attach the Worker to a route or custom domain, for example `rates.example.com`. Two details matter here:

- **Sorting the targets** makes `EUR,GBP` and `GBP,EUR` the same cache entry.
- **The Cache API is per data centre** and does not work on `workers.dev` subdomains, so use a custom domain. Each data centre fetches at most once a minute per distinct query.

Your front end now calls the proxy, and the key stays on the server:

```
const res = await fetch("https://rates.example.com/rates?source=USD&target=EUR,GBP");
const rows = await res.json();          // an array, even for one pair
const eur = rows.find((r) => r.target === "EUR").rate;
```

## Alternative: a Next.js route handler on Vercel

If your site is already a Next.js app, a route handler does the same job without a second service. Put the key in a server-only environment variable named `ALLRATES_API_KEY`, without the `NEXT_PUBLIC_` prefix.

```
// app/api/rates/route.ts
const ALLOWED = new Set(["USD", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF", "INR"]);

export async function GET(req: Request) {
  const params = new URL(req.url).searchParams;
  const source = (params.get("source") ?? "USD").toUpperCase();
  const targets = [...new Set((params.get("target") ?? "").toUpperCase().split(",").filter(Boolean))].sort();

  if (!ALLOWED.has(source) || targets.length === 0 || targets.length > 10 ||
      !targets.every((t) => ALLOWED.has(t))) {
    return Response.json({ error: "currency not allowed" }, { status: 400 });
  }

  const res = await fetch(
    `https://allratestoday.com/api/v1/rates?source=${source}&target=${targets.join(",")}`,
    {
      headers: { Authorization: `Bearer ${process.env.ALLRATES_API_KEY}` },
      next: { revalidate: 60 },
    }
  );
  if (!res.ok) return Response.json({ error: "upstream unavailable" }, { status: 502 });

  return Response.json(await res.json(), {
    headers: { "Cache-Control": "public, s-maxage=60, stale-while-revalidate=30" },
  });
}
```

Because the page and the API share an origin, you do not need CORS headers at all. `next: { revalidate: 60 }` keeps the upstream response in the data cache for a minute, and `s-maxage=60` lets the CDN serve repeat requests without invoking the function.

For rate limiting on Vercel, use the platform's firewall rate-limit rules, or a Redis counter such as `@upstash/ratelimit` keyed on the client IP. Either way, the limit belongs in front of the upstream call.

## CORS is not access control

It is tempting to think that `Access-Control-Allow-Origin` stops other people from using your proxy. It does not. CORS only tells browsers whether a page on another origin may read the response. A script running `curl` ignores it completely.

That is why the allowlist and the rate limit matter more than CORS. The allowlist stops your proxy from becoming a general-purpose rate service, and the rate limit caps how fast anyone, browser or not, can trigger upstream calls. Combined with the 60-second cache, the worst case is bounded: with 10 allowed targets, the number of distinct queries is finite, and each is fetched at most once a minute per data centre.

## Budget the proxy, not the users

With the cache in place, your request count depends on the number of distinct queries and the cache time, not on your traffic. One base with all your currencies in one call, cached for 60 seconds in one region, is at most 1,440 calls a day, about 43,200 in 30 days. That fits the Large plan. Cache for 10 minutes and it drops to about 4,320, which fits Small at 5,000 a month. See [pricing](https://allratestoday.com/pricing/) and [how to cache exchange rates](https://allratestoday.com/blog/how-to-cache-exchange-rates-avoid-rate-limits/) for more options.

If you are still deciding on a provider, the [free currency API comparison](https://allratestoday.com/blog/best-free-currency-exchange-api-2026/) covers limits and update frequency.

## Rotate the key

If a key has ever been in a public bundle, a Git commit or a mobile build, treat it as leaked. Rotation takes a minute:

1. Open the AllRatesToday dashboard and use the **rotate** button to issue a new key.
2. Update the secret: `npx wrangler secret put ALLRATES_API_KEY`, or the environment variable in Vercel followed by a redeploy.
3. Remove the old key from your code and from your Git history if it was committed.

This is the other benefit of a proxy: rotation is a server change. You never need to ship a new app build to replace a key.

## FAQ

### Is it safe to use a free exchange rate API key in frontend JavaScript?

No. Anything in browser code is public, including environment variables inlined at build time. Keep the key on a server or a Worker and call that from the browser.

### Does CORS protect my API key or proxy?

No. CORS controls which web pages can read a response in a browser. Scripts and curl ignore it. Use a currency allowlist and per-IP rate limiting to protect the proxy.

### How long should the proxy cache exchange rates?

60 seconds matches how often the rates refresh. If you need fewer upstream calls, cache for several minutes; most price displays do not need second-by-second data.

### What should I do if my key was exposed?

Rotate it from the AllRatesToday dashboard, update the secret in your Worker or hosting environment, and remove the old key from your code and Git history.

### Can I use the proxy for a mobile app?

Yes. The same proxy works for iOS and Android. The app calls your proxy URL and never contains the key.

## Keep your key on the server

Get a free API key, put it in a Worker secret, and rotate it from the dashboard whenever you need to.

 [Get your free API key](https://allratestoday.com/register)

## Related Articles

- [Currency converter API in Swift and Kotlin](https://allratestoday.com/blog/currency-api-swift-kotlin-mobile/)
- [Build an FX rates dashboard](https://allratestoday.com/blog/fx-rates-dashboard-api/)
- [How to cache exchange rates and avoid rate limits](https://allratestoday.com/blog/how-to-cache-exchange-rates-avoid-rate-limits/)
- [Free currency converter API in JavaScript](https://allratestoday.com/blog/free-currency-converter-api-javascript/)
- [Free vs paid exchange rate APIs](https://allratestoday.com/blog/free-vs-paid-exchange-rate-apis/)
- [API documentation](https://allratestoday.com/docs/)
