@billingextensions/sdk
v2.0.0
Published
TypeScript SDK for BillingExtensions - MV3-safe browser extension billing
Maintainers
Readme
BillingExtensionsSDK
Accept payments in your Chrome extension (subscriptions + paid access) with a simple SDK that stays in sync without requiring a content script.
// background.js (service worker)
const client = BillingExtensionsSDK.createBillingExtensionsClient({
appId: "my-new-app",
publicKey: "app_publicKey",
});
client.enableBackgroundStatusTracking();Menu
- Secure server-side API
- Important setup order
- Install
- Required Chrome permissions
- Quick start (MV3 service worker)
- Using the SDK
- How it works
- No content script required
- Instant updates (optional content script)
- Types
- Full API Reference
- Troubleshooting
- License / Support
Secure server-side API (optional — the SDK works without this)
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
Important setup order (don’t skip this)
- Sign up to BillingExtensions (https://billingextensions.com)
- Connect Stripe in the BillingExtensions dashboard
- Create your App (your extension)
- Create your Plans (subscriptions / tiers)
- Add the SDK to your extension and initialize it
Take these steps before following the rest of this guide.
Install
Option A — npm (recommended)
npm install @billingextensions/sdkInit (recommended)
The init script scaffolds the minimum setup for you:
- adds required
permissions(storage) inmanifest.json - adds required
host_permissions(if needed) - detects your existing service worker setup:
- classic (uses
importScripts) vs module (ESMimport/export) - and chooses the right integration automatically
- classic (uses
- 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)
npx -y -p @billingextensions/sdk bext init <appId> <publicKey>Optional flags
# Force classic service worker (importScripts + IIFE build)
npx -y -p @billingextensions/sdk billingextensions init <appId> <publicKey> --classic
# Force module service worker (type="module" + ESM)
npx -y -p @billingextensions/sdk billingextensions init <appId> <publicKey> --module
# Use npm import (requires a bundler/build step; module mode only)
npx -y -p @billingextensions/sdk billingextensions init <appId> <publicKey> --module --npm
# Override the service worker path
npx -y -p @billingextensions/sdk billingextensions init <appId> <publicKey> --sw background/service-worker.jsNotes
--npmgeneratesimport * as BillingExtensionsSDK from "@billingextensions/sdk"— this only works if your service worker is bundled (Chrome can’t resolve npm specifiers at runtime).- If your service worker already uses
importScripts(...), init will default to classic and will not setbackground.type = "module".
Notes:
--npmgeneratesimport * as BillingExtensionsSDK from "@billingextensions/sdk"— this only works if your service worker is bundled (Chrome can’t resolve npm specifiers at runtime).- If your service worker already uses
importScripts(...), init will default to classic and will not setbackground.type = "module".
You can still set everything up manually if you prefer — init is just a shortcut.
Option B — drop in the dist file (no npm / no build step)
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
Classic build for MV3 service workers usingimportScripts(...)(globalBillingExtensionsSDK). Can also be used as a content script for instant checkout updates.dist/BillingExtensionsSDK.module.js
ESM build forbackground.type = "module"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.
Before you start: required Chrome permissions - (already done if you ran the init script)
BillingExtensionsSDK uses Chrome storage for caching and cross-context sync.
Add this to your manifest.json before initializing the client:
{
"permissions": ["storage"],
"host_permissions": ["https://billingextensions.com/*"]
}Note:
host_permissionsshould match the BillingExtensions API domain your extension calls.
Quick start (MV3 service worker) - (already done if you ran the init script)
This is the typical “background-first” setup.
// 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 });
});Using the SDK
Gating paid features
const status = await client.getUser();
if (!status.paid) {
await client.openManageBilling();
return;
}
// ✅ user is paidReturns (Promise<UserStatus>) — 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
UserStatuscomes from the BillingExtensions OpenAPI schema (components["schemas"]["UserStatus"]).
Listening for updates
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 | nulldiff: StatusDiff
StatusDiff meaning
entitlementChanged— paid access changedplanChanged— plan info changed (id,nickname,status,currentPeriodEnd)usageChanged— usage info changed (used,limit,resetsAt)
Open billing / manage subscription (if the user has paid / subscribed, use this to open up a url for them to manage the subscription)
await client.openManageBilling();Returns
Promise<void>
Under the hood the SDK creates a paywall session and opens response.url in a new tab.
Get available plans
const plans = await client.getPlans();
console.log(plans);Returns (Promise<PlansForSdk[]>)
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 & background tracking
AutoSync (enabled by default)
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:
client.enableBackgroundStatusTracking();This does two things:
- Warms the cache — kicks off an initial refresh so
getUser()returns instantly when the popup opens - Listens for content script messages — if you add the optional content script, enables instant post-checkout updates (see Instant updates)
Force refresh (skip caches)
const status1 = await client.getUser({ forceRefresh: true });
const status2 = await client.refresh();Returns
Promise<UserStatus>
How it works (in plain English)
- 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.storageso 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)
No content script required (default)
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.
Instant updates (optional content script)
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.
Usage
Option 1: IIFE format (BillingExtensionsSDK.js)
"content_scripts": [
{
"matches": ["https://billingextensions.com/*"],
"js": ["BillingExtensionsSDK.js"],
"run_at": "document_start"
}
]Option 2: ESM format (BillingExtensionsSDK.module.js)
"content_scripts": [
{
"matches": ["https://billingextensions.com/*"],
"js": ["BillingExtensionsSDK.module.js"],
"run_at": "document_start",
"type": "module"
}
]Types
These are the types used in the README (from your SDK’s types.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"];Full API Reference
BillingExtensionsSDK.createBillingExtensionsClient(config)
Creates a configured client.
Params
config.appId: string(required)config.publicKey: string(required)
Returns
BillingExtensionsClient
client.getUser(opts?)
Fetch the current user status (cached, with SWR-style revalidation).
Options
forceRefresh?: boolean
Returns
Promise<UserStatus>
Key fields used by the SDK:
paid: booleanplan: object | nullusage: object | null | undefined
client.refresh()
Force a fresh status fetch from the API and update the cache.
Returns
Promise<UserStatus>
client.openManageBilling()
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<void>
client.onStatusChanged(handler)
Subscribe to status updates across all extension contexts.
Handler
StatusChangeHandler(next, prev, diff)
Returns
() => voidunsubscribe function
client.enableAutoSync(opts?)
Enable automatic background syncing (enabled by default).
Returns
void
client.disableAutoSync()
Disable AutoSync.
Returns
void
client.enableBackgroundStatusTracking()
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
client.getPlans()
Fetch the list of plans configured for your app.
Returns
Promise<PlanForSDK[]>
Troubleshooting
“My UI didn’t update after checkout”
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.
“Can i add a free trial period?”
Yes! With the latest version of the SDK, you can now add free trial periods to your applications!
“I’m seeing localhost URLs”
If your billing URL points to localhost in production:
- verify the app/environment base URL configuration in your dashboard/backend,
- and ensure you’re using the correct environment variables.
