Claude API Browser Extension Integration Guide
If you're trying to wire Claude into a Chrome or Firefox extension, you've probably already hit the wall: calling api.anthropic.com directly from a content script or extension popup either fails on CORS, or forces you to ship your API key inside the extension bundle — which anyone can extract with a few minutes and devtools.
This article covers the actual integration pattern that works for browser extensions: a thin backend between your extension and Claude, how to structure the Manifest V3 permissions and messaging, and how to stream Claude's response into a popup or sidebar without blocking the UI thread.
Why you can't call Claude directly from an extension
Two separate problems show up the moment you try:
- CORS: Anthropic's API isn't designed to be called from arbitrary browser origins. Even if a request technically goes through, you're relying on undocumented behavior that can change.
- Key exposure: Extensions ship as unpacked JS/HTML/JSON that users (or malicious actors) can open and read. Any secret key hardcoded or bundled into the extension is effectively public. Browser extension stores have flagged and removed extensions for exactly this.
The fix isn't a CORS workaround — it's moving the Claude call server-side and having the extension talk to that server instead.
The architecture that actually works
Extension (popup/content script)
│ fetch() with your app's own key
▼
Backend proxy (your server or SubToAPI)
│ Authorization: Bearer <real Claude credential>
▼
Claude
The extension never touches an Anthropic key. It talks to an HTTPS endpoint — either your own backend or a managed layer like SubToAPI — using a key that's scoped to your application, easy to revoke, and safe to embed because it has no admin privileges and can be rate-limited per install.
SubToAPI is built for exactly this pattern: it turns your Claude access into a standard HTTPS API with sub_live_... keys, so your extension calls a normal REST endpoint instead of managing Anthropic auth, CORS, and streaming plumbing yourself.
Manifest V3 setup
Declare the host permission for your backend (not for anthropic.com — your extension never calls it):
{
"manifest_version": 3,
"name": "Claude Sidebar Assistant",
"version": "1.0",
"background": { "service_worker": "background.js" },
"permissions": ["storage"],
"host_permissions": ["https://api.subtoapi.app/*"],
"action": { "default_popup": "popup.html" }
}
Keep the actual request in the background service worker, not the content script. Content scripts run in the context of the page and are more exposed; the background worker is a cleaner boundary for making network calls and holding short-lived tokens in memory.
Calling the API from the background worker
// background.js
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type !== "ASK_CLAUDE") return;
fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${msg.appKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-3-5-sonnet-20241022",
max_tokens: 1024,
messages: [{ role: "user", content: msg.prompt }]
})
})
.then(res => res.json())
.then(data => sendResponse({ ok: true, data }))
.catch(err => sendResponse({ ok: false, error: err.message }));
return true; // keep the message channel open for async response
});
The popup sends a message with the user's prompt and receives the result — it never sees or stores the Anthropic credential, only a scoped application key. Full request/response shape is in the messages docs.
Streaming into the popup UI
A non-streaming popup feels laggy for anything longer than a sentence. Extensions support fetch with streaming bodies in the background worker, and you can relay chunks to the popup via chrome.runtime.Port:
// background.js
chrome.runtime.onConnect.addListener(port => {
port.onMessage.addListener(async (msg) => {
const res = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${msg.appKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-3-5-sonnet-20241022",
max_tokens: 1024,
stream: true,
messages: [{ role: "user", content: msg.prompt }]
})
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
port.postMessage({ chunk: decoder.decode(value) });
}
port.postMessage({ done: true });
});
});
On the popup side, connect with chrome.runtime.connect() and append each chunk to the DOM as it arrives. Details on the event format and reconnect behavior are in the streaming guide.
Storing the application key safely
Don't hardcode even a scoped app key in source. Instead:
- Have the extension prompt the user to paste their key once, on install.
- Store it with
chrome.storage.local(notlocalStorage, which content scripts can read across origins in some edge cases). - Let users revoke/rotate the key from your dashboard without reinstalling the extension — this is why app-scoped keys (not raw provider credentials) matter so much for distributed clients like extensions.
If you're the one issuing these keys to your users, a dashboard that supports per-key revocation and usage tracking saves you from writing that infrastructure yourself. That's the core of what SubToAPI's key management covers alongside the API itself.
Handling errors and rate limits gracefully
Browser extensions run on unpredictable networks (corporate proxies, VPNs, flaky Wi-Fi) and users will click the action button repeatedly if nothing visibly happens. At minimum:
- Debounce the "ask" action in the popup so duplicate requests aren't fired on double-clicks.
- Show distinct UI states for network failure vs. rate-limit (429) vs. invalid key (401) — don't collapse them into one generic error toast.
- Set a reasonable
max_tokensso a single request doesn't tie up the service worker for too long; Chrome can suspend idle service workers mid-request on some versions.
Getting started
To wire this up end to end: sign up at /signup, generate an app key, and follow the quickstart to send your first request from a local script before moving the call into your extension's background worker. Pricing starts at €9/month for the Solo plan, with Team and Scale tiers for shared or production workloads — see /pricing.
How do I avoid CORS errors calling Claude from a browser extension?
You don't fight CORS — you route the request through a backend (your own server or a service like SubToAPI) that calls Claude server-side and returns JSON or a stream to your extension over a normal HTTPS endpoint.
Is it safe to store an Anthropic API key inside a browser extension?
No. Extension code is readable by anyone who installs it. Use a scoped, revocable application key issued by a backend proxy instead, and keep the real provider credential server-side only.
Can a Manifest V3 extension stream Claude responses in real time?
Yes — make the streaming fetch request in the background service worker and relay chunks to the popup or content script using chrome.runtime.Port messaging as they arrive.