Your OpenAI or Anthropic key is in the browser. Now what?
Revoke the key now, before you change any code. OpenAI says exposing your API key in client-side environments like browsers or mobile apps allows malicious users to take that key and make requests on your behalf. Anthropic compares an API key to a credit card number, because whoever uses it incurs charges on your behalf.
What to do right now
- If you believe your key has been leaked, OpenAI says to rotate it immediately from the API Keys page. If you suspect your Claude API key may be compromised, Anthropic recommends revoking it immediately.
- Create a new key and store it only in your server settings or your host's secrets.
- Look at the provider's usage page for requests you did not make.
How to move the AI call to your server
The browser should call your own route, and your route calls the provider. OpenAI says requests should always be routed through your own backend server, where you can keep your API key secure. A route that forwards anything for anyone is just a new way to spend your credit, so the route below refuses visitors who are not signed in, caps the question, fixes the model on the server and sends back only the answer.
// app/api/answer/route.js on atlas.example, server only
import { getSignedInUser } from '@/lib/auth' // your app's own sign-in check
const MAX_QUESTION_LENGTH = 2000
export async function POST(request) {
const user = await getSignedInUser(request)
if (!user) {
return Response.json({ error: 'Sign in first' }, { status: 401 })
}
const { question } = await request.json()
if (typeof question !== 'string' || question.length > MAX_QUESTION_LENGTH) {
return Response.json({ error: 'Ask a shorter question' }, { status: 400 })
}
const response = await fetch('https://api.openai.com/v1/responses', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ model: process.env.OPENAI_MODEL, input: question }),
})
if (!response.ok) {
return Response.json({ error: 'Try again later' }, { status: 502 })
}
const data = await response.json()
const answer = (data.output ?? [])
.flatMap(item => item.content ?? [])
.filter(part => part.type === 'output_text')
.map(part => part.text)
.join('')
return Response.json({ answer })
}The browser sends only the question, and the key never leaves the server. Replace getSignedInUser with the sign-in check your app already has. Then add a per-user rate limit, and set a usage limit in your provider's dashboard, so one signed-in account cannot run up your bill either.
Did it work?
Deploy, then call /api/answer while signed out. It should refuse with the sign-in error, not a reply from the model. Then run a quick check on the live address. Atlas, our example app at atlas.example, would pass once its scripts no longer carry the key. A clean check does not prove nobody copied the old key, which is why revoking it comes first.
Questions people ask
Can I hide the key with obfuscation or a build trick?
No. Anything the browser can use, a visitor can read. Only your server can keep it private.
Is a server route enough on its own?
It keeps the key private, but the route itself can be abused. Require sign-in or add a rate limit so strangers cannot spend your credit through it.
See what your site shows from outside
A quick check reads your homepage the way a visitor would and gives you a score. No account needed.
Check your siteSources
- API Key Best Practices, Keeping Your Keys Safe and Secure Claude Help Center, read on October 6, 2026
- Best Practices for API Key Safety OpenAI Help Center, read on October 6, 2026