SDK Reference
Complete API reference for @crxpay/sdk.
Installation
npm install @crxpay/sdk
Imports
// Background service worker
import { CrxPay } from '@crxpay/sdk/background';
// Popup, options page, or sidepanel
import { CrxPay } from '@crxpay/sdk';
// Content script
import { CrxPay } from '@crxpay/sdk/content';
CrxPay.configure(options)
Context: Background only
Initialize the SDK. Must be called once in your background service worker before any other SDK methods.
const client = CrxPay.configure({
apiKey: string, // Required. Public API key from dashboard.
refreshInterval?: 'hourly' | 'daily' | number, // Default: 'hourly'. Minutes if number.
debug?: boolean, // Log cache hits/misses/transitions. Default: false.
baseUrl?: string, // Override API URL (for self-hosting).
testMode?: boolean | 'auto', // 'auto' detects unpacked extensions.
onSubscriptionUpdated?: (sub: Subscription) => void,
});
Returns: CrxPayBackground instance with handleMessage() and onPaid.
CrxPay.identify(email)
Context: All (background, popup, content)
Associates the extension user with an email address. Must be called before the user can subscribe.
const result = await CrxPay.identify('user@example.com');
// Result<void>
| Field | Type | Description |
|---|---|---|
email | string | User's email address |
Returns: Result<void> — { ok: true } on success.
CrxPay.getSubscription()
Context: All
Returns the current subscription state. Reads from the signed local cache first; falls back to network if stale (> 4 hours) or missing.
const result = await CrxPay.getSubscription();
if (result.ok) {
const { status, isActive, hasEntitlement, entitlements } = result.data;
}
Returns: Result<Subscription>
Subscription object
| Field | Type | Description |
|---|---|---|
status | SubscriptionStatus | Current state (see below) |
isActive | boolean | true for: active, trialing, cancelled_but_valid |
isInGracePeriod | boolean | true when payment failed but in grace period |
currentPeriodEnd | Date | null | When current billing period ends |
cancelAtPeriodEnd | boolean | Whether subscription cancels at period end |
trialEnd | Date | null | Trial expiry date |
pausedAt | Date | null | When subscription was paused |
hasEntitlement(id) | (string) => boolean | Check if user has a specific entitlement |
entitlements | Entitlement[] | All active entitlements |
_cachedAt | Date | When this data was cached |
_source | 'cache' | 'network' | Where this data came from |
SubscriptionStatus
| Status | Description | isActive |
|---|---|---|
none | No subscription record | false |
trialing | In free trial | true |
active | Paid and current | true |
past_due | Payment failed, in grace period | false |
cancelled_but_valid | Cancelled but period hasn't ended | true |
paused | Subscription paused | false |
expired | Fully expired | false |
unknown | Network failed and no cache | false |
CrxPay.syncSubscription()
Context: All
Forces a network refresh, bypassing the local cache. Call this immediately after a checkout completes.
const result = await CrxPay.syncSubscription();
Returns: Result<Subscription>
CrxPay.getOfferings()
Context: All
Fetches the current products and prices configured in the dashboard. Use this to build dynamic paywalls without hardcoding prices.
const result = await CrxPay.getOfferings();
if (result.ok) {
const { current, all } = result.data;
const monthly = current?.packages.find(p => p.type === 'monthly');
const annual = current?.packages.find(p => p.type === 'annual');
}
Returns: Result<Offerings>
Offerings object
| Field | Type | Description |
|---|---|---|
current | Offering | null | The default offering |
all | Record<string, Offering> | All offerings by identifier |
Package types
'monthly' | 'annual' | 'lifetime' | 'weekly' | 'custom'
CrxPay.setAttributes(attributes)
Context: All
Sets key-value metadata on the current customer for analytics segmentation.
await CrxPay.setAttributes({
source: 'product_hunt',
plan_tier: 'power_user',
ab_test: 'paywall_v2',
});
Returns: Result<void>
CrxPay.openCheckout(priceId?)
Context: All
Opens Stripe Checkout in a new tab. Pass a priceId to charge a specific price, or omit to use the default offering.
// Specific price
await CrxPay.openCheckout('price_abc123');
// Default offering
await CrxPay.openCheckout();
Returns: Result<void>
CrxPay.startTrial({ plan })
Context: All
Thin wrapper over openCheckout that reads as "start the trial for this plan." Trial length, whether a card is collected, and what happens at trial end are all configured on the price itself in the dashboard — so the SDK call stays the same when you change trial settings later.
await CrxPay.startTrial({ plan: 'price_abc123' });
Under the hood this is identical to CrxPay.openCheckout('price_abc123'). Use whichever reads better in your code. See the trials guide for how to configure the price.
Returns: Result<void>
CrxPay.previewPlanChange(newPriceId)
Context: All
Returns the Stripe proration preview for switching the user's active subscription to a different price, without actually charging them. Use this to render a "you'll be charged $X today" confirmation before calling changePlan.
const res = await CrxPay.previewPlanChange('price_annual_pro');
if (res.ok) {
const dollars = (res.data.amountDue / 100).toFixed(2);
showConfirm(`You'll be charged $${dollars} today, then billed annually.`);
}
Returns: Result<{ amountDue: number; amountRemaining: number; currency: string; prorationDate: number; lines: Array<…> }>
The amount fields are in the smallest currency unit (cents for USD). Lines are individual invoice line items — credits show up as negative amounts.
CrxPay.changePlan(newPriceId)
Context: All
Switch the active subscription to a new plan with proration. Stripe issues a prorated invoice immediately for the difference; the SDK polls for the resulting webhook so getSubscription() reflects the new plan within a few seconds.
const res = await CrxPay.changePlan('price_annual_pro');
if (res.ok) toast('Plan updated — thanks!');
Requires an active subscription (status active, trialing, or past_due). Returns no_active_subscription otherwise.
Returns: Result<{ ok: boolean; processorSubscriptionId: string; status: string }>
CrxPay.openBillingPortal()
Context: All
Opens the Stripe Customer Portal where users can cancel, upgrade, or update their payment method.
await CrxPay.openBillingPortal();
Returns: Result<void>
CrxPay.logout()
Context: All
Clears local auth state, subscription cache, and anonymous install ID.
await CrxPay.logout();
Returns: Result<void>
CrxPay.onPaid
Context: All
Event emitter that fires when the subscription transitions from a non-paid state to active or trialing.
// Add listener (returns unsubscribe function)
const unsubscribe = CrxPay.onPaid.addListener((subscription) => {
showThankYou();
if (subscription.hasEntitlement('pro')) {
enablePro();
}
});
// Remove listener
unsubscribe();
// or
CrxPay.onPaid.removeListener(myHandler);
In background context, listeners are called in-process. In popup/content contexts, listeners receive the event via chrome.runtime.onMessage broadcast.
Error handling
The SDK never throws. All methods return a typed Result<T>:
type Result<T> =
| { ok: true; data: T }
| { ok: false; error: { code: CrxPayErrorCode; message: string } };
Error codes
| Code | Description |
|---|---|
NETWORK_UNAVAILABLE | Fell back to cache (not a fatal error) |
CACHE_TAMPERED | HMAC verification failed — cache discarded |
INVALID_API_KEY | 401 from API |
CUSTOMER_NOT_IDENTIFIED | Must call identify() first |
PAYMENT_PROCESSOR_ERROR | Stripe returned an error |
RATE_LIMITED | 429 from API |
EXTENSION_CONTEXT_INVALID | Extension was reloaded mid-call |
Test mode
Enable test mode to use Stripe test data:
CrxPay.configure({
apiKey: 'crxpay_pub_...',
testMode: true, // always use test mode
// or
testMode: 'auto', // auto-detect unpacked extensions
});
When testMode: 'auto', the SDK checks if chrome.runtime.getManifest().update_url is present. Unpacked extensions don't have this field, so they automatically use test mode.
Was this page helpful?
Your feedback shapes what we document next.