Build a Chrome Extension That Calls the OpenAI API
Use AI inside your extension safely: where to store the API key, how to call OpenAI from a service worker, and how to handle rate limits.

One of the most popular Chrome extension categories in 2026 is 'AI-powered' — summarizers, rewriters, translators, code explainers. Almost all of them call an LLM API from inside the extension.
Where to store the API key
- Never hardcode it in the manifest or source files — anyone can unzip your extension.
- Use `chrome.storage.local` for user-supplied keys (each user pastes their own).
- Use a backend proxy for keys you own — your extension calls your server, your server calls OpenAI.
- ManifestGo's secrets vault handles the proxy automatically.
Calling OpenAI from a service worker
Service workers can use `fetch` directly — no need for content scripts or message passing for the network call itself. The pattern is: content script captures user selection → posts to service worker → service worker calls OpenAI → returns the response.
Handling rate limits and errors
- Wrap every fetch in try/catch — network errors are the most common failure mode.
- Respect 429 responses with exponential backoff.
- Show a clear error in the popup, not a silent failure.
Exact fetch pattern from a service worker
A minimal working call looks like: fetch("https://api.openai.com/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer " + apiKey }, body: JSON.stringify({ model: "gpt-4o-mini", messages: [...] }) }). Because MV3 service workers can't use XMLHttpRequest, fetch with async/await inside a chrome.runtime.onMessage listener is the standard pattern, and the listener must return true to keep the message channel open for the async response.
The host_permissions entry OpenAI calls require
Add "https://api.openai.com/*" to host_permissions in manifest.json. Without it, fetch throws "Failed to fetch" with no CORS detail in the service worker console, which is a frequently misdiagnosed error — developers assume it's an OpenAI-side CORS block when it's actually a missing manifest permission.
Message-passing between content script and service worker
- Content script captures the selection with window.getSelection().toString() and sends it via chrome.runtime.sendMessage({ type: "SUMMARIZE", text }).
- Service worker's chrome.runtime.onMessage listener checks message.type, calls the OpenAI API, and calls sendResponse(result) — but only after returning true synchronously to keep the channel alive.
- For popup-to-worker communication instead of content-script, the same sendMessage/onMessage pattern applies; only the sender context differs.
- For streaming responses, use a chrome.runtime.Port connection instead of one-shot messages, since sendMessage doesn't support incremental chunks well.
Rate limits and the exact status codes to handle
- 429 Too Many Requests — back off exponentially starting around 1 second, doubling up to a capped max, and read the Retry-After header if present.
- 401 Unauthorized — the API key is invalid, revoked, or missing the Bearer prefix; surface this distinctly from a network error so users know to re-enter their key.
- 500/503 — OpenAI-side outage; retry once after a short delay, then show a clear 'service unavailable, try again' message rather than looping silently.
- Context length errors (400 with a message mentioning maximum context length) — truncate the input text client-side before resending rather than failing outright.
Why a backend proxy beats a client-stored key for most products
If you're paying for API usage yourself (rather than each user bringing their own key), never ship your key inside the extension — unzipping any Chrome extension reveals every string in its source, including a hardcoded key. Instead, route calls through a small backend you control that holds the real OpenAI key server-side and enforces per-user quotas; the extension only ever talks to your backend over HTTPS.
If your API key would embarrass you on a public GitHub repo, it shouldn't be sitting in an unzipped Chrome extension either — both are equally readable.
CSP considerations for API-calling extensions
The default extension_pages CSP (script-src 'self'; object-src 'self') doesn't restrict fetch destinations — that's controlled by host_permissions, not CSP. But if your popup renders API responses as HTML (say, Markdown-rendered summaries), you must sanitize before inserting into the DOM, since the CSP won't stop a script-injection XSS from within your own rendered content.
Frequently asked questions
Why does my OpenAI call work in a regular webpage but fail from the extension's service worker?
Service workers require the API's origin to be explicitly listed in host_permissions in manifest.json; a webpage isn't bound by that manifest, which is why the same fetch call behaves differently in each context.
Can I stream OpenAI's response token-by-token into a Chrome extension popup?
Yes, using the OpenAI streaming API with server-sent events, but you need to relay chunks from the service worker to the popup via a chrome.runtime.Port rather than a single sendMessage/sendResponse pair.
Does using chrome.storage.session help with API costs?
Indirectly — caching recent API responses in chrome.storage.session (which clears when the browser closes) avoids re-calling the API for the same input during a single browsing session, reducing redundant paid requests.