Claude API Browser Extension Integration Guide
Claude API Browser Extension Integration Guide
If you're building a browser extension that calls the Claude API — a writing assistant, a page summarizer, a research sidebar — you'll hit three problems almost immediately: where to store your API key safely, how to call the API from a content script or popup without tripping CORS, and how to stream responses into the extension UI without blocking the browser. This guide covers all three, with working code for Manifest V3.
The short answer: never call Anthropic's API directly from a content script or popup with a hardcoded key. Route the call through your extension's background service worker (or better, through a backend proxy), handle the response as a stream, and inject the result into the page via messaging. Below is the full pattern, plus a simpler alternative using an API gateway like SubToAPI that removes the key-management problem entirely.
Why you can't just call the API from a content script
Content scripts run in the context of the page the user is visiting, which means:
- Anything you hardcode in a content script is visible to anyone who inspects the extension's source (unpacked extensions are trivially unzippable).
- Anthropic's API requires server-side style auth headers — shipping a raw API key inside a public extension is the fastest way to get it scraped and abused within hours.
- Direct
fetch()calls from a content script can be blocked by the page's own Content Security Policy, which you don't control.
The correct architecture is: content script → background service worker → API. The background worker is isolated from the page, holds the credentials, and is the only place that talks to the network.
Step 1: Manifest and permissions
Manifest V3 extensions need host_permissions for the API domain and a background service worker declared explicitly.
{
"manifest_version": 3,
"name": "Claude Sidebar",
"version": "1.0.0",
"permissions": ["storage", "activeTab", "scripting"],
"host_permissions": ["https://api.subtoapi.app/*"],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html"
}
}
If you're calling Anthropic's API directly instead of through a proxy, you'd list https://api.anthropic.com/* here — but see the security note below before doing that in anything you distribute publicly.
Step 2: Where to store the key
Never ship the key inside the bundled extension code. Two realistic options:
- User-supplied key, stored with
chrome.storage.local(notsync, so it doesn't leave the device via Google's sync infrastructure). The user pastes their own key into your options page, and your extension never embeds a shared secret. - Backend proxy, where the extension sends requests to your own server, and your server holds the credentials. This is the only safe option if you want a single key shared across all installs of your extension.
For option 2, a gateway like SubToAPI is a good fit because it gives you scoped, revocable keys (sub_live_...) per user or per install, instead of one shared Anthropic key that you'd have to rotate manually if it leaks. You create keys from the dashboard after signing up at /signup and swap them out per customer without touching your extension's code.
Step 3: The background service worker
// background.js
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type !== "ASK_CLAUDE") return;
(async () => {
const { apiKey } = await chrome.storage.local.get("apiKey");
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-3-5-sonnet",
max_tokens: 1024,
messages: [{ role: "user", content: msg.prompt }]
})
});
const data = await res.json();
sendResponse(data);
})();
return true; // keep the message channel open for async response
});
And the content script or popup that triggers it:
chrome.runtime.sendMessage(
{ type: "ASK_CLAUDE", prompt: "Summarize this page in 3 bullet points." },
(response) => {
console.log(response);
}
);
This keeps the network call and the API key entirely inside the extension's privileged background context. See /docs/quickstart and /docs/messages for the full request and response shape.
Step 4: Streaming into the UI
For sidebar or chat-style UIs, waiting for the full response feels slow. Streaming requires reading the response body as it arrives, which service workers support via ReadableStream.
async function streamToClaude(prompt, onChunk) {
const { apiKey } = await chrome.storage.local.get("apiKey");
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-3-5-sonnet",
max_tokens: 1024,
stream: true,
messages: [{ role: "user", content: prompt }]
})
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
onChunk(decoder.decode(value));
}
}
Forward each chunk to the popup or injected panel with chrome.runtime.sendMessage as it arrives, rather than waiting for the full stream to close. Details on event formats are in /docs/streaming.
Step 5: Tool use for page-aware actions
If your extension needs Claude to act on page content — extract structured data, fill a form, trigger a DOM action — define tools in your request and handle the tool_use block in the background worker before relaying results back to the content script. This keeps the model's reasoning server-side while letting the content script execute the actual DOM mutation safely. The tool schema and response format are covered in /docs/tools.
Handling CORS and manifest review
Chrome Web Store and Firefox Add-ons reviewers both flag extensions that embed secrets in client-visible code, so keep credentials in chrome.storage or behind your own backend. CORS is a non-issue once calls are made from the background service worker with the right host_permissions declared — it's content scripts and popups making direct cross-origin fetches that get blocked by page CSPs, not the background context.
Picking between a direct key and a gateway
A single developer key works fine for a personal tool. Once you're distributing the extension to other people or selling it, you need per-user keys, usage visibility, and the ability to revoke access without redeploying. SubToAPI issues separate sub_live_... keys per customer, exposes usage metadata per key, and lets you manage team seats from one dashboard — useful if your extension has multiple paying users hitting the same backend. Compare plans on /pricing.
questions
Can I call the Claude API directly from a content script? Technically yes with the right host permissions, but you'd have to expose your API key in client-side code that's visible to anyone unpacking the extension. Route calls through the background service worker instead.
Do I need a backend server, or can the extension call the API alone? A pure client-side extension works if each user supplies their own API key via an options page. If you want one shared key across all your users, you need a backend proxy or an API gateway to issue scoped keys.
How do I stream Claude responses into a popup UI? Read the fetch response body with ReadableStream in the background worker, decode chunks as they arrive, and forward each chunk to the popup via chrome.runtime.sendMessage instead of waiting for the full response.