Call OpenAI, Notion, and Google Sheets from the browser without leaking your API key
APIs like OpenAI, Notion, and Google Sheets need a secret, and anything in client-side JavaScript is public. This post shows how to keep that secret out of the browser without writing a backend, and what the setup still can't protect.
The secret key is the real constraint
Some APIs send CORS headers and some don't. You can check any endpoint by sending a request with an Origin header and looking for Access-Control-Allow-Origin in the response. But even when an API does allow browser calls, it doesn't solve the bigger problem: 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, and can only call OpenAI:
POST /v1/api-keys
Authorization: Bearer <jwt>
Content-Type: application/json
{
"name": "Chat widget",
"allowed_origins": ["https://app.example.com"],
"allowed_targets": ["api.openai.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 Host, Origin, Referer, Authorization, Cookie, and X-API-Key, 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 origin locking is browser-only: a script outside a browser can send anyOriginheader. On the Free plan, keys also always accepthttp://localhost:*andhttp://127.0.0.1:*, and a key with no allowed origin accepts any origin. What limits the damage is a narrowpath_prefix, your daily quota, and a spending limit set with the provider. - Upstream rules don't restrict where the key can go. They only decide where your secret is injected. Without
allowed_targets, a leaked key can still proxy to any public host on your quota. Setallowed_targetsto the one host you meant (as in the OpenAI example above). - 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
Create a free key and store your first upstream secret from the dashboard.