Every endpoint answers cross-origin requests, so a fetch straight from browser JavaScript
works on the first try. That is the trap. It works, it ships, and the key is now in a bundle
that every visitor downloads — view-source, the network tab, or a scrape of your JS assets
all return it. Minification is not obfuscation, and a .env file does not help once the value
is inlined at build time.
A leaked key spends your credits until you notice. This guide is the fix: a small proxy you own, holding the key, in front of the calls your front end makes.
What the proxy is for
The key is the reason to build it, but it is not the only thing you get:
| Why it matters | |
|---|---|
| The key stays server-side | The only place it exists is your platform's secret store |
| You choose what is callable | One endpoint, not all 368 — a stolen proxy URL is worth far less than a stolen key |
| Caching | Repeat lookups cost you nothing instead of a credit each |
| Rate limiting | A runaway retry loop in someone's browser stops at your edge, not at your balance |
That last pair is why a proxy usually pays for itself even on a project where the key exposure would have been survivable.
Cloudflare Workers
Put the key in a secret — wrangler secret put APIVERVE_API_KEY — and never in wrangler.toml.
export default {
async fetch(request, env) {
const url = new URL(request.url);
// Allow one endpoint. An open passthrough is the key leak with extra steps.
if (url.pathname !== '/api/validate') {
return new Response('Not found', { status: 404 });
}
const email = url.searchParams.get('email');
if (!email) return new Response('email is required', { status: 400 });
const upstream = new URL('https://api.apiverve.com/v1/emailvalidator');
upstream.searchParams.set('email', email);
const res = await fetch(upstream, {
headers: { 'x-api-key': env.APIVERVE_API_KEY },
cf: { cacheTtl: 3600, cacheEverything: true },
});
return new Response(res.body, {
status: res.status,
headers: {
'content-type': 'application/json',
// Your origin, not '*'. This proxy is for your page.
'access-control-allow-origin': 'https://example.com',
'cache-control': 'public, max-age=3600',
},
});
},
};The cf.cacheTtl line is doing real work: identical lookups inside the hour are served from
Cloudflare's cache and never reach us, so they never cost a credit.
Vercel Edge Functions
Same shape. Set APIVERVE_API_KEY in the project's environment variables — without the
NEXT_PUBLIC_ prefix, which is what publishes a value to the browser.
// app/api/validate/route.js
export const runtime = 'edge';
export async function GET(request) {
const email = new URL(request.url).searchParams.get('email');
if (!email) {
return Response.json({ error: 'email is required' }, { status: 400 });
}
const res = await fetch(
`https://api.apiverve.com/v1/emailvalidator?email=${encodeURIComponent(email)}`,
{
headers: { 'x-api-key': process.env.APIVERVE_API_KEY },
next: { revalidate: 3600 },
},
);
return Response.json(await res.json(), { status: res.status });
}Your front end now calls /api/validate?email=… on your own origin. No key, no CORS
configuration, and nothing to leak.
The temptation is a catch-all that rewrites /api/* onto api.apiverve.com/v1/* and forwards
whatever arrives. That is an unauthenticated public copy of your account — anyone who finds the
URL can call every endpoint on your credits. Name the endpoints you actually use.
Pass through only what you need
Two rules make the difference between a proxy and an open relay:
Validate the input. The parameters come from a browser, which means they come from anyone. Check them before they reach us — a proxy that forwards an unbounded string is how a single visitor spends a month of credits.
Do not echo the whole response. Our responses carry fields your page does not render, and
x-api-remaining-credits tells a stranger exactly how much there is left to burn. Return the
fields you use.
When the key has to be public
Sometimes there is no server — a static demo, a CodePen, an internal tool nobody will host. Then the key is public on purpose, and the job is to bound the damage rather than prevent it:
- Issue a sub-key, never the account key
- Scope it to the one endpoint the page calls
- Cap its rate and its credit ceiling, so the worst case is a number you picked
- Rotate it on a schedule — see key rotation
For a lookup form on a marketing page, an embedded form is the better answer again: it authenticates with a form token, proxies server-side, and adds a domain allow-list and a CAPTCHA, with no key in the page at all.
Before you ship
grep -ryour built bundle for the key, not your source — build tools inline values- Confirm the proxy answers only the paths you named, and 404s the rest
- Confirm
access-control-allow-originnames your origin rather than* - If a key was ever in a committed file, rotate it — git history is public the moment the repo is
Next
Security is the wider picture: where keys live, what the transport guarantees, and what to check before a release. Key scoping covers bounding a key that must exist in a risky place.