← Blog

Claude API for Browser Extension Development

2026-09-30 · 5 min read · SubToAPI Team

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:

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:

  1. Your own server — full control, you issue per-user tokens, you handle billing and rate limits yourself. Most work, most flexibility.
  2. 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.
  3. 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

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.

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 →