← Blog

Claude API Browser Extension Integration Guide

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

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:

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:

  1. User-supplied key, stored with chrome.storage.local (not sync, 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.
  2. 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.

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 →