How to Build a Browser Extension With Claude API
Why You Can't Just Call Claude Directly From a Content Script
The short answer: you need a background service worker that holds your API credentials and makes the network request, plus message passing to get results into your popup or content script. You cannot call the Anthropic API straight from a content script injected into a webpage — browsers block it for security reasons, and even if they didn't, shipping a raw API key inside an extension bundle is a guaranteed way to get it scraped and abused within hours.
This matters because most "Claude extension" tutorials gloss over the two hard parts: where the API call actually lives, and how you keep the key safe once your extension is in the Chrome Web Store and installed on thousands of machines. This guide covers both, along with the manifest structure, message passing, and streaming setup you need for a responsive UI.
Architecture for a Claude-Powered Extension
A typical Manifest V3 extension that uses Claude has three pieces:
- Content script — reads page content (selected text, DOM nodes, form fields) and sends it to the background worker.
- Background service worker — the only place that talks to the network. It holds the API key (or token) and makes the request to Claude.
- Popup or side panel UI — renders the response, handles streaming chunks, shows loading state.
Keeping all network calls in the service worker is not optional in Manifest V3 — it's how the platform is designed, and it conveniently solves the "don't expose your key to the page" problem at the same time, since content scripts never see the credential.
Step 1: Manifest and Permissions
{
"manifest_version": 3,
"name": "Claude Page Assistant",
"version": "1.0.0",
"permissions": ["storage", "activeTab", "scripting"],
"host_permissions": ["https://api.subtoapi.app/*"],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"]
}
]
}
The host_permissions entry matters: Chrome will block fetch requests from a service worker to a host that isn't declared here, even though service workers aren't subject to the same CORS restrictions as a page.
Step 2: Background Service Worker That Talks to Claude
The service worker listens for messages from the popup, calls the API, and sends the result back:
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.apiKey}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4-5",
max_tokens: 500,
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
});
Notice this calls api.subtoapi.app rather than Anthropic's endpoint directly. The reasoning is simple: you still need an HTTPS endpoint with normal API semantics, but you don't want to manage a separate backend just to proxy requests and hide secrets. SubToAPI turns your existing Claude access into a standard HTTPS API with its own sub_live_... application keys, so the extension talks to a regular REST endpoint instead of needing a custom server you have to deploy and maintain. See the quickstart for the exact request/response shape.
Step 3: Wiring Up the Popup or Content Script
document.getElementById("ask").addEventListener("click", async () => {
const prompt = document.getElementById("prompt").value;
const result = document.getElementById("result");
result.textContent = "Thinking...";
chrome.runtime.sendMessage(
{ type: "ASK_CLAUDE", prompt, apiKey: await getStoredKey() },
(response) => {
if (response.ok) {
result.textContent = response.data.content[0].text;
} else {
result.textContent = `Error: ${response.error}`;
}
}
);
});
Store the API key with chrome.storage.local, not in the manifest or source code, and let each user paste their own key during onboarding. That way a leaked extension bundle doesn't leak a shared credential.
Step 4: Streaming Responses Into the UI
For anything longer than a short answer, streaming makes the extension feel instant instead of frozen. Service workers can read streamed responses just like a page can:
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-5",
max_tokens: 500,
stream: true,
messages: [{ role: "user", content: prompt }]
})
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let full = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
full += decoder.decode(value);
chrome.runtime.sendMessage({ type: "STREAM_CHUNK", text: full });
}
Forward each chunk to the popup via chrome.runtime.sendMessage and append it to the DOM as it arrives. Details on the event format are in the streaming docs.
Step 5: Letting Claude Act on the Page
If your extension should summarize a page, fill out a form, or extract structured data, pass the DOM content gathered by the content script as part of the prompt, or define a tool Claude can call to request specific page data mid-conversation. Tool use is useful here because it lets you keep the first request small and only fetch extra DOM context when Claude actually asks for it — see tool use for the schema.
Keeping API Keys Out of the Client — for Real
Even with per-user keys stored locally, think about scope: don't hand an extension a key with billing or org-admin rights. Issue a scoped, lower-privilege key per install if your platform supports it, rotate keys from a dashboard, and track usage so you notice anomalies fast (a key suddenly making 10,000 requests a minute is a sign it leaked). Check the pricing page if you're deciding between a Solo key for a side-project extension versus Team seats for something you're shipping with collaborators.
Distributing and Testing
Load the extension unpacked via chrome://extensions with Developer Mode on while building. Test the background worker's network calls with the DevTools "Service Workers" inspector — standard console.log in a service worker shows up there, not in the popup's console. Once it works, zip the extension folder and submit it through the Chrome Web Store developer dashboard; Manifest V3 review focuses heavily on permissions, so only request host_permissions for domains you actually call.
Questions
Can I call the Anthropic API directly from a Chrome extension without a backend? Not safely from a content script or page context due to CORS, and not securely from anywhere if it means bundling a raw API key in the extension. A service worker calling an HTTPS API endpoint (your own, or a managed one like SubToAPI) is the standard pattern.
Do I need a server to build a Claude-powered extension? You need an HTTPS endpoint that accepts your requests and talks to Claude — that can be a server you deploy, or a hosted API layer like SubToAPI's /v1/messages endpoint, which skips the deployment step by turning your Claude access into a ready HTTPS API with per-key authentication.
How do I stream Claude's response into a browser extension popup? Fetch with stream: true in the service worker, read the response body as a stream, and forward chunks to the popup with chrome.runtime.sendMessage so the UI updates incrementally instead of waiting for the full response.