← Blog

How to Build a Browser Extension With Claude API

2026-10-29 · 5 min read · SubToAPI Team

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:

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.

Turn your Claude access into an HTTPS API

SubToAPI gives you application API keys, streaming, tool use and usage insights on top of your existing Claude access — set up in minutes.

Start free  Read the quickstart →