← Blog

Claude API Browser Extension Development Guide

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

Building a browser extension that talks to Claude means solving two problems at once: how to call the API from a content script or background worker without leaking credentials to every page you inject into, and how to deal with the fact that Anthropic's API does not set permissive CORS headers for direct browser calls. This article walks through both, plus the manifest permissions, message passing, and streaming patterns you need for a working Chrome or Firefox extension.

If you only need the short answer: you cannot call api.anthropic.com directly from a content script with a hardcoded key (it will be exposed in the bundle and may be blocked by CORS depending on the request). The standard approach is to route requests through your extension's background service worker, and either proxy through your own backend or through a gateway like SubToAPI that issues scoped, revocable keys and handles CORS and streaming for you. Below is the full pattern.

Why a direct fetch() call from a content script won't work well

A few concrete issues:

The fix for all three is the same: don't call the model API from the page context. Call it from a background service worker that holds a short-lived or scoped key, and have your content scripts/popup talk to the background worker via chrome.runtime.sendMessage.

Recommended architecture

content script / popup  →  chrome.runtime.sendMessage
        ↓
background service worker  →  HTTPS request → API gateway (your backend or SubToAPI)
        ↓
        response streamed back via chrome.runtime.Port

This keeps your key out of injected page contexts entirely. If you're shipping the extension publicly, route through your own backend or a service like SubToAPI rather than embedding any secret in the extension package — extension bundles are not a secure place to store credentials, even in the background worker, since determined users can still unpack and read them.

manifest.json essentials

{
  "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" }
}

Only list the host you actually call under host_permissions. If you proxy through SubToAPI, that's https://api.subtoapi.app/; if you proxy through your own server, list your domain instead. Never request https://api.anthropic.com/ directly for a browser extension — you won't use it that way.

Background worker calling the API

// background.js
chrome.runtime.onMessage.addListener(async (msg, sender, sendResponse) => {
  if (msg.type !== "ASK_CLAUDE") return;

  const res = await fetch("https://api.subtoapi.app/v1/messages", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${await getStoredKey()}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      model: "claude-sonnet-4-5",
      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
});

Keys should be stored with chrome.storage.local (or synced storage for cross-device use), entered by the user in an options page rather than baked into the bundle. If you're building this for your own team's internal tooling, a SubToAPI application key scoped to a specific project makes rotation straightforward — you can revoke one extension's key without touching others. See the quickstart for key creation.

Streaming into a popup or sidebar

Extensions feel much more responsive with streamed tokens, especially in a side panel UI. Service workers can't hold open a ReadableStream indefinitely across suspension, so the usual pattern is: open a long-lived port between the popup and the background worker, and pipe chunks through as they arrive.

// 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 ${await getStoredKey()}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({
        model: "claude-sonnet-4-5",
        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 });
  });
});
// popup.js
const port = chrome.runtime.connect({ name: "claude-stream" });
port.postMessage({ prompt: userInput });
port.onMessage.addListener((msg) => {
  if (msg.chunk) appendToOutput(msg.chunk);
});

Details on the event format for streamed responses are in the streaming docs.

Letting Claude act on the page

A common extension use case is summarizing the active tab or filling a form based on model output. Extract the page content in the content script, pass it through sendMessage to the background worker, and keep the actual DOM manipulation in the content script — don't try to inject HTML from the model response without sanitizing it first. If you want Claude to decide which action to take (summarize vs. extract vs. translate), tool use is a cleaner fit than parsing free text; see the tools docs for the schema format.

Handling rate limits and team usage

If multiple people on your team use the same internal extension, give each person their own API key rather than sharing one. This avoids one heavy user starving everyone else's rate limit and gives you per-person usage visibility in a dashboard. SubToAPI's pricing includes per-seat keys on the Team and Scale plans specifically for this kind of shared internal tooling.

Publishing checklist

Before submitting to the Chrome Web Store or AMO:

questions

Can I call the Claude API directly from a content script? Not reliably. CORS and key-exposure issues mean you should route calls through a background service worker and a server-side proxy or API gateway instead.

How do I stream Claude responses into a popup UI? Use a chrome.runtime.connect port between the popup and background worker, read the fetch response body as a stream in the background worker, and forward decoded chunks over the port as they arrive.

Do I need a backend to ship a Claude-powered extension? Yes, in some form — either your own server or a gateway like SubToAPI that issues per-user keys, handles CORS, and lets you avoid bundling secrets into the extension package.

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 →