Claude API for Browser Extension Development
Building a browser extension that calls the Claude API means solving two problems most tutorials skip: you can't safely ship an Anthropic API key inside an extension bundle, and direct calls from a content script or popup will hit CORS restrictions. This article covers both, plus the manifest permissions, message-passing pattern, and streaming setup you actually need to ship a working extension.
If you're searching for "claude api for browser extension development," you're probably past the prototype stage where you hardcoded a key in a .env file and are now realizing that model doesn't work once the code ships to users. The short answer: extensions should never embed a raw Anthropic key client-side. Instead, route requests through a backend or a proxy service, and use chrome.runtime messaging to keep API calls out of content scripts.
Why you can't call the Claude API directly from a content script
Content scripts run in the context of the page they're injected into, which means:
- CORS:
api.anthropic.comdoesn't allow arbitrary origin requests, so browser fetch calls from a content script or popup will fail unless you're going through an approved extension origin and correct headers. - Key exposure: anything in your extension's JS bundle is readable by anyone who unpacks the
.crxfile. A hardcodedsk-ant-...key will get scraped and abused within days of publishing to the Chrome Web Store. - Rate limiting and billing: if every user's extension calls Claude with your own key, you're paying for and rate-limiting against every install, with no way to attribute usage per user.
The standard fix is a background service worker that proxies requests to a backend you control, which then talks to Claude (or a Claude-backed API layer) server-side.
Extension architecture that works
A typical Manifest V3 extension calling an LLM looks like this:
popup.js / content.js → background.js (service worker) → your backend → Claude API
The content script or popup never talks to the network directly for AI calls — it sends a message to the background worker, which does the fetch.
manifest.json permissions:
{
"manifest_version": 3,
"permissions": ["storage"],
"host_permissions": ["https://api.subtoapi.app/*"],
"background": {
"service_worker": "background.js"
}
}
content.js — sending a request from the page context:
chrome.runtime.sendMessage(
{ type: "ASK_CLAUDE", prompt: "Summarize this page's selected text" },
(response) => {
console.log(response.text);
}
);
background.js — the only place that touches the network:
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 " + BACKEND_ISSUED_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4",
max_tokens: 500,
messages: [{ role: "user", content: msg.prompt }]
})
})
.then((r) => r.json())
.then((data) => sendResponse({ text: data.content[0].text }))
.catch((err) => sendResponse({ error: err.message }));
return true; // keeps the message channel open for async sendResponse
});
Note the return true — without it, sendResponse fires after the listener has already returned and the promise gets dropped silently.
Where does the key actually live?
You have three real options for the backend piece:
- Your own server — full control, you issue per-user tokens, you handle billing and rate limits yourself. Most work, most flexibility.
- A managed API layer — a service like SubToAPI sits between your extension and Claude, giving you application-scoped keys (
sub_live_...) instead of raw Anthropic credentials, so you can issue a distinct key per extension build or per customer without managing your own proxy infrastructure. This is the fastest path if you don't want to run and secure a backend just to hide one API key. - A thin serverless function you deploy yourself that wraps the Claude API and enforces per-user rate limits, still requiring you to manage secrets and scaling.
Whichever route you pick, the extension code above doesn't change much — you're just swapping the URL and auth header.
Streaming responses inside a popup
Extension popups close when the user clicks away, so long streaming responses need to either write to chrome.storage as they arrive or stream into a persistent side panel instead of a popup. If you're using Manifest V3's side panel API, you can keep a stream open the same way you would in a normal web app:
const res = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": "Bearer " + apiKey,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4",
max_tokens: 1024,
stream: true,
messages: [{ role: "user", content: prompt }]
})
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let output = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
output += decoder.decode(value, { stream: true });
renderToSidePanel(output);
}
This runs fine from a background service worker or a side panel document, both of which persist longer than a popup. See /docs/streaming for the full event format if you're building this against SubToAPI.
Rate limits and per-user attribution
If your extension has more than a handful of users, you need to know who's burning through tokens. Issuing a separate application API key per user or per install — rather than one shared key for the whole extension — lets you track usage, throttle abusers, and revoke access without redeploying. SubToAPI's dashboard exposes usage metadata per key, which maps cleanly onto "per-user" if you generate a key at signup and store it against their account. Check /docs/quickstart for the key-issuing flow and /pricing for how seat-based billing works if you're distributing to a team rather than the public.
Permissions checklist before you publish
- Only request
host_permissionsfor the domains you actually call — Chrome Web Store review flags overly broad permissions. - Never bundle a raw Anthropic key in
background.js; even service workers can be inspected viachrome://extensionsin dev mode. - Use
chrome.storage.local(notlocalStorage) for anything you cache client-side, since content scripts and background workers don't sharelocalStorage. - Test with the extension loaded unpacked and DevTools open on the service worker to catch failed fetches before submitting for review.
FAQs
Can I call the Claude API directly from a Chrome extension's fetch, no backend? Not safely. You'd need to embed your API key in the shipped code, which anyone can extract from the unpacked extension. Always route through a backend or proxy that holds the real credential.
Does Manifest V3 block network requests to third-party APIs? No, but you must declare the domain under host_permissions in manifest.json, and the request should originate from the background service worker rather than a content script to avoid CORS issues on the host page.
How do I keep a streaming response alive when a popup closes? Move the fetch and stream handling into the background service worker or a persistent side panel, then push updates to the UI via chrome.runtime messages or chrome.storage, since popups are torn down on blur and can't hold an open stream.