Call OpenAI, Notion, and Google Sheets from the browser without leaking your API key
These three APIs all send CORS headers, so a plain fetch() from the browser goes through. The problem is the key: anything in client-side JavaScript is public. Here's how to keep it out of the browser without writing a backend, and what that setup still can't protect.
CORS isn't the blocker here, the key is
OpenAI, Notion, and Google's APIs all return Access-Control-Allow-Origin. Check it yourself:
curl -sI -H "Origin: https://example.com" https://api.notion.com/v1/users | grep -i access-control-allow-origin
access-control-allow-origin: *
So the request works. The catch is that anything you ship to the browser, including bundled code, environment variables baked in at build time, and headers visible in devtools, can be read by any visitor. An OpenAI key in a React app is someone else's free GPU budget, and a Notion integration token gives access to every page the integration was shared with.
The usual fix is a small backend that holds the secret and forwards the call. corsproxy.dev's managed upstream headers do the same thing without you running the backend: you store the secret on your corsproxy.dev API key once, and the proxy adds it to requests going to the host you name.
OpenAI
Create a key that injects your OpenAI secret, but only for one path:
POST /v1/api-keys
Authorization: Bearer <jwt>
Content-Type: application/json
{
"name": "Chat widget",
"allowed_origins": ["https://app.example.com"],
"upstream_rules": [{
"target_host": "api.openai.com",
"path_prefix": "/v1/chat/completions",
"headers": { "Authorization": "Bearer sk-YOUR_OPENAI_KEY" }
}]
}
// Browser: no OpenAI key anywhere in this code
const target = encodeURIComponent('https://api.openai.com/v1/chat/completions');
const res = await fetch(`https://api.corsproxy.dev/proxy?url=${target}&key=YOUR_CORSPROXY_KEY`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: 'gpt-5-mini',
messages: [{ role: 'user', content: 'Hello' }]
})
});
const data = await res.json();
A narrow path_prefix matters. If it were just /v1/, anyone holding your corsproxy.dev key could reach every OpenAI endpoint with your secret, including file uploads and fine-tuning.
Notion
Notion needs two headers: Authorization and Notion-Version. Only the first one is secret. The proxy passes your browser's own request headers through (except Authorization, Origin, Referer, and Host), so send the version from the client and inject only the token. That also fits the free plan's limit of one injected header per key.
"upstream_rules": [{
"target_host": "api.notion.com",
"path_prefix": "/v1/databases/",
"headers": { "Authorization": "Bearer ntn_YOUR_NOTION_TOKEN" }
}]
const target = encodeURIComponent('https://api.notion.com/v1/databases/DATABASE_ID/query');
const res = await fetch(`https://api.corsproxy.dev/proxy?url=${target}&key=YOUR_CORSPROXY_KEY`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Notion-Version': '2022-06-28' },
body: JSON.stringify({ page_size: 10 })
});
const { results } = await res.json();
Google Sheets
Google has its own answer here: restrict the API key to your site's HTTP referrers in Google Cloud Console, and a key that's visible in the browser matters much less. Do that first. If you'd rather the key not appear in the browser at all, Google APIs also accept it in the X-Goog-Api-Key header instead of ?key=, so you can inject it:
"upstream_rules": [{
"target_host": "sheets.googleapis.com",
"path_prefix": "/v4/spreadsheets/SHEET_ID/",
"headers": { "X-Goog-Api-Key": "YOUR_GOOGLE_API_KEY" }
}]
const target = encodeURIComponent(
'https://sheets.googleapis.com/v4/spreadsheets/SHEET_ID/values/A1:C10'
);
const res = await fetch(`https://api.corsproxy.dev/proxy?url=${target}&key=YOUR_CORSPROXY_KEY`);
const { values } = await res.json();
What this doesn't protect
- Your corsproxy.dev key is still public. Visitors can't read the injected secret, but anyone who copies your corsproxy.dev key can use it within the host and path you allowed.
allowed_originsstops other websites from using it in a browser, but a script outside a browser can fake theOriginheader. What limits the damage is a narrowpath_prefix, your daily quota, and a spending limit set with the provider. - Keys that go in the URL can't be hidden this way. Only headers are injected. APIs that accept the key only as a query parameter, such as OpenWeather's
appid, still need a real backend if the key matters. - Per-user authorization is your job. Everyone visiting your site shares one upstream identity. If users should only see their own data, you need a backend that knows who they are.
When you don't need any of this
If the API is yours, add CORS headers on your own server (see our Express CORS guide) and keep secrets there. If the API needs no key and already sends CORS headers, call it directly; a proxy only adds latency. Full field reference: API docs → Managed upstream headers.
Try it
Get a free API key — 500 requests/day, no credit card. Store your first upstream secret in a couple of minutes.