Home → Help

CORS error calling an AI API from the browser

Call the API from your own server, not from the page. The CORS block is doing its job: a request the browser can make is a request any visitor can read, including the key in its Authorization header.

What you are seeing

Why it happens

It is tempting to read this as a configuration problem — add an origin, ship the feature. But work through what a successful browser call means. The key travels in a header the user's own devtools display in full. It is in the bundle if you inlined it, and in the network tab if you did not. Anyone who opens the page has it.

Keys leaked this way are not a theoretical risk. Public code search surfaces them continuously, and a stolen key against a pay-per-token API converts directly into someone else's bill on your account. This is why most providers set a restrictive allow-origin rather than * — not an oversight, a deliberate refusal.

The proxy pattern is three or four lines of server code: your page calls your backend, your backend holds the key and calls the provider. It also gives you the place to put per-user rate limits and spend caps, which you want anyway.

Confirm it is this

Ask the endpoint what origins it actually allows:

curl -s -I -X OPTIONS 'YOUR_BASE_URL/chat/completions' \
  -H 'Origin: https://example.com' \
  -H 'Access-Control-Request-Method: POST' \
  | grep -i 'access-control'

If Access-Control-Allow-Origin comes back as a specific domain rather than *, browser calls from your origin are not going to work, and no amount of client-side change will alter that.

How to fix it

  1. Put one endpoint on your own backendYour page posts to /api/chat on your own domain; that handler adds the Authorization header and forwards the request. No CORS involved, because the browser only ever talks to your origin.
  2. Never use a build-time public prefix for a keyNEXT_PUBLIC_, VITE_ and REACT_APP_ variables are compiled into the JavaScript you ship. A key placed there is published, not configured.
  3. Rotate anything that has already shippedIf a key has been in a deployed bundle, treat it as public and replace it. Rotation is cheap; an unbounded bill is not.
  4. Add per-user limits at your proxyOnce traffic passes through your own handler you can cap spend per session. Calling the provider directly from the browser gives you nowhere to enforce that.
APICLAN's API host allows only https://apiclan.us as an origin, deliberately — keys are meant to live on a server. Base URL setup for server-side clients is in the quickstart.

Related

Unexpected token '<' when calling an OpenAI-compatible API401 invalid API key — when the key looks right but still fails

Last checked 2026-10-01. Written from problems diagnosed on a live OpenAI-compatible gateway, not collected from other sites.