TypeScript SDK for BillingExtensions - MV3-safe browser extension billing
npm install @billingextensions/sdkAccept payments in your Chrome extension (subscriptions + paid access) with a simple SDK that stays in sync without requiring a content script.
``js
// background.js (service worker)
const client = BillingExtensionsSDK.createBillingExtensionsClient({
appId: "my-new-app",
publicKey: "app_publicKey",
});
client.enableBackgroundStatusTracking();
`
---
- Secure server-side API
- Important setup order
- Install
- Option A — npm
- Init
- Option B — drop in the dist file
- Required Chrome permissions
- Quick start (MV3 service worker)
- Using the SDK
- Gating paid features
- Listening for updates
- Open billing / manage subscription
- Get available plans
- AutoSync & background tracking
- Force refresh (skip caches)
- How it works
- No content script required
- Instant updates (optional content script)
- Types
- Full API Reference
- Troubleshooting
- License / Support
---
The SDK is designed to be secure even if you don’t run a backend. However, if your extension has a backend (recommended for anything sensitive), you can verify subscription status server-side using the BillingExtensions API: https://billingextensions.com/docs.
This is useful when you need to:
- gate paid features securely (don’t trust the client alone)
- protect expensive operations (e.g. LLM calls)
- keep your own database in sync with BillingExtensions/Stripe
- stop sending subscription status from the extension to your backend — your server can check it directly via HTTPS whenever it needs to
---
1) Sign up to BillingExtensions (https://billingextensions.com)
2) Connect Stripe in the BillingExtensions dashboard
3) Create your App (your extension)
4) Create your Plans (subscriptions / tiers)
5) Add the SDK to your extension and initialize it
> Take these steps before following the rest of this guide.
---
`bash`
npm install @billingextensions/sdk
The init script scaffolds the minimum setup for you:
- adds required permissions (storage) in manifest.jsonhost_permissions
- adds required (if needed)importScripts
- detects your existing service worker setup:
- classic (uses ) vs module (ESM import/export)
- and chooses the right integration automatically
- injects a ready-to-run MV3 service worker snippet
- (vendored mode) copies the right prebuilt SDK file next to your service worker
#### Quick start (works for any extension — no npm project required)
`bash`
npx -y -p @billingextensions/sdk bext init
#### Optional flags
Advanced: init flags (optional)
`bash
# Force classic service worker (importScripts + IIFE build)
npx -y -p @billingextensions/sdk billingextensions init
# Force module service worker (type="module" + ESM)
npx -y -p @billingextensions/sdk billingextensions init
# Use npm import (requires a bundler/build step; module mode only)
npx -y -p @billingextensions/sdk billingextensions init
# Override the service worker path
npx -y -p @billingextensions/sdk billingextensions init
`
Notes
- --npm generates import * as BillingExtensionsSDK from "@billingextensions/sdk" — this only works if your service worker is bundled (Chrome can’t resolve npm specifiers at runtime).importScripts(...)
- If your service worker already uses , init will default to classic and will not set background.type = "module".
> Notes:
> - --npm generates import * as BillingExtensionsSDK from "@billingextensions/sdk" — this only works if your service worker is bundled (Chrome can’t resolve npm specifiers at runtime).importScripts(...)
> - If your service worker already uses , init will default to classic and will not set background.type = "module".
> You can still set everything up manually if you prefer — init is just a shortcut.
---
If you don’t want npm or a build step, copy the prebuilt file(s) into your extension and reference them directly:
- dist/BillingExtensionsSDK.js importScripts(...)
Classic build for MV3 service workers using (global BillingExtensionsSDK). Can also be used as a content script for instant checkout updates.
- dist/BillingExtensionsSDK.module.js background.type = "module"
ESM build for service workers (import via a relative path). Can also be used as a content script for instant checkout updates.
- dist/index.cjs / dist/index.js
Bundler/Node builds (CJS/ESM) if you’re importing via a build tool.
---
BillingExtensionsSDK uses Chrome storage for caching and cross-context sync.
Add this to your manifest.json before initializing the client:
`json`
{
"permissions": ["storage"],
"host_permissions": ["https://billingextensions.com/*"]
}
> Note: host_permissions should match the BillingExtensions API domain your extension calls.
---
This is the typical “background-first” setup.
`js
// background.js (service worker)
import BillingExtensionsSDK from "@billingextensions/sdk";
const client = BillingExtensionsSDK.createBillingExtensionsClient({
appId: "my-new-app",
publicKey: "app_ENNSXktPl1kOxQ2bQbb96",
});
client.enableBackgroundStatusTracking();
// ✅ Your “listener” (like extpay.onPaid)
client.onStatusChanged((next, prev, diff) => {
if (!prev?.paid && next.paid) {
console.log("User paid! ✅", next);
buildContextMenu();
}
buildContextMenu();
console.log("status change", { diff, prev, next });
});
`
---
`js
const status = await client.getUser();
if (!status.paid) {
await client.openManageBilling();
return;
}
// ✅ user is paid
`Promise
Returns () — key fields (as used by the SDK)extensionUserId: string
- — @description Unique identifier for this extension userpaid: boolean
- — @description Whether the user has an active paid subscriptionsubscriptionStatus: string
- — @description Subscription status: none, active, trialing, past_due, canceled.plan: PlanType (See plans below)
- — @description Current plan info, or null if no subscription.currentPeriodEnd: string | null
- - @description End of current billing period (ISO 8601)cancelAtPeriodEnd: boolean
- - @description Whether the subscription will cancel at period endonTrial: boolean
- - @description Whether or not the user is currently on a free trial periodtrialEnd: Date | null
- - @description The date in which the users free trial will end (if applicable)
> The full shape of UserStatus comes from the BillingExtensions OpenAPI schema (components["schemas"]["UserStatus"]).
---
`js
const unsubscribe = client.onStatusChanged((next, prev, diff) => {
if (!prev?.paid && next.paid) console.log("Upgraded ✅");
if (prev?.paid && !next.paid) console.log("Downgraded ❌");
console.log(diff);
});
// later
unsubscribe();
`
Handler args
- next: UserStatusprev: UserStatus | null
- diff: StatusDiff
-
StatusDiff meaning
- entitlementChanged — paid access changedplanChanged
- — plan info changed (id, nickname, status, currentPeriodEnd)usageChanged
- — usage info changed (used, limit, resetsAt)
---
`js`
await client.openManageBilling();
Returns
- Promise
Under the hood the SDK creates a paywall session and opens response.url in a new tab.
---
`js`
const plans = await client.getPlans();
console.log(plans);
Returns (Promise)
- id: string — @description Unique identifier for this planname: string
- — @description Plan namepriceAmount: number
- — @description Price in smallest currency unit (e.g., cents)currency: string
- - @description ISO 4217 currency code (e.g., usd)billingType: string
- - @description Billing type: one_time or recurringinterval: string | null
- - @description Billing interval: month, year, etc. (null for one_time)intervalCount: number
- - @description Number of intervals between billings
---
#### AutoSync (enabled by default)
`js
client.enableAutoSync({
// AutoSyncOptions (see DEFAULT_AUTOSYNC_OPTIONS)
});
client.disableAutoSync();
`
#### Background status tracking (recommended)
Call this in your service worker to warm the cache on startup (regardless of the content script). If you add a content script, it will not work without this:
`js`
client.enableBackgroundStatusTracking();
This does two things:
1. Warms the cache — kicks off an initial refresh so getUser() returns instantly when the popup opens
2. Listens for content script messages — if you add the optional content script, enables instant post-checkout updates (see Instant updates)
---
`js`
const status1 = await client.getUser({ forceRefresh: true });
const status2 = await client.refresh();
Returns
- Promise
---
- The SDK fetches the user’s status from the BillingExtensions API.
- It caches status briefly (TTL ~30s) to keep things fast.
- It writes status into chrome.storage so every extension context stays in sync.
- Updates happen via:
- AutoSync (enabled by default) — refreshes on focus, visibility, and network changes
- Optional instant refresh messaging from the content script (if you add it)
---
By default, you do not need a content script.
In normal flows, the user pays, Stripe refreshes/redirects, and when the user opens your extension again the SDK will fetch the latest status right away.
---
If you want the UI to update instantly even while the extension UI stays open during checkout, you can add the SDK as a content script. The SDK automatically detects when it's running in a content script context and listens for checkout success.
This is optional on purpose:
- Adding a content script often triggers extra Chrome warnings and can make the review process take longer.
- BillingExtensionsSDK defaults to a no-content-script approach to reduce review friction.
Option 1: IIFE format (BillingExtensionsSDK.js)
`json`
"content_scripts": [
{
"matches": ["https://billingextensions.com/*"],
"js": ["BillingExtensionsSDK.js"],
"run_at": "document_start"
}
]
Option 2: ESM format (BillingExtensionsSDK.module.js)
`json`
"content_scripts": [
{
"matches": ["https://billingextensions.com/*"],
"js": ["BillingExtensionsSDK.module.js"],
"run_at": "document_start",
"type": "module"
}
]
---
These are the types used in the README (from your SDK’s types.ts).
`ts
export type BillingExtensionsClientConfig = {
/* Immutable app ID from the BillingExtensions dashboard /
appId: string;
/* Publishable public key /
publicKey: string;
};
export type GetUserOptions = {
/* Force refresh from API, ignoring cache (default: false) /
forceRefresh?: boolean;
};
export type StatusDiff = {
/* True if entitled status changed /
entitlementChanged: boolean;
/* True if plan info changed (id, nickname, status, or currentPeriodEnd) /
planChanged: boolean;
/* True if usage info changed (used, limit, or resetsAt) /
usageChanged: boolean;
};
export type StatusChangeHandler = (
next: UserStatus,
prev: UserStatus | null,
diff: StatusDiff
) => void;
// OpenAPI-backed (authoritative shapes)
export type PlanForSDK = components["schemas"]["Plan"];
export type UserStatus = components["schemas"]["UserStatus"];
`
---
Creates a configured client.
Params
- config.appId: string (required)config.publicKey: string
- (required)
Returns
- BillingExtensionsClient
---
Fetch the current user status (cached, with SWR-style revalidation).
Options
- forceRefresh?: boolean
Returns
- Promise
Key fields used by the SDK:
- paid: booleanplan: object | null
- usage: object | null | undefined
-
---
Force a fresh status fetch from the API and update the cache.
Returns
- Promise
---
Open checkout page / manage subscription in a new tab (if the user has paid / subscribed, use this to open up a url for them to manage the subscription)
Returns
- Promise
---
Subscribe to status updates across all extension contexts.
Handler
- StatusChangeHandler(next, prev, diff)
Returns
- () => void unsubscribe function
---
Enable automatic background syncing (enabled by default).
Returns
- void
---
Disable AutoSync.
Returns
- void
---
Enable background tracking. Recommended to call in your service worker.
- Warms the cache with an initial refresh so getUser() is fast when the popup opens
- Sets up a message listener for the optional content script's checkout return notification
Returns
- void
---
Fetch the list of plans configured for your app.
Returns
- Promise
---
In most cases the update will feel instant.
That’s because Stripe typically reloads/redirects after payment, and when the user opens your extension again the SDK will fetch the latest status right away (and also revalidate in the background).
If the user keeps your extension UI open the whole time (e.g. they pay in another tab and never close the popup/options page), the status will update when they next focus the popup (via AutoSync) or close/reopen it.
If you want truly instant updates even while the extension UI stays open, you can add the optional content script build — but it’s optional on purpose:
- Adding a content script often triggers extra Chrome warnings and can make the review process take longer.
- BillingExtensionsSDK defaults to a no-content-script approach to reduce review friction.
Yes! With the latest version of the SDK, you can now add free trial periods to your applications!
---