Docs/Guides/Hide your API key behind a proxy

Hide your API key behind a proxy

A key in front-end code is readable by everyone who loads the page. Two working proxies — Cloudflare Workers and Vercel Edge — plus what to do when the key genuinely has to be public.

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-sideThe only place it exists is your platform's secret store
You choose what is callableOne endpoint, not all 368 — a stolen proxy URL is worth far less than a stolen key
CachingRepeat lookups cost you nothing instead of a credit each
Rate limitingA 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.

js
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.

js
// 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.

Do not forward the whole request

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 -r your 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-origin names 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.

Was this page helpful?

Last updated