crxpaydocs

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>
FieldTypeDescription
emailstringUser'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

FieldTypeDescription
statusSubscriptionStatusCurrent state (see below)
isActivebooleantrue for: active, trialing, cancelled_but_valid
isInGracePeriodbooleantrue when payment failed but in grace period
currentPeriodEndDate | nullWhen current billing period ends
cancelAtPeriodEndbooleanWhether subscription cancels at period end
trialEndDate | nullTrial expiry date
pausedAtDate | nullWhen subscription was paused
hasEntitlement(id)(string) => booleanCheck if user has a specific entitlement
entitlementsEntitlement[]All active entitlements
_cachedAtDateWhen this data was cached
_source'cache' | 'network'Where this data came from

SubscriptionStatus

StatusDescriptionisActive
noneNo subscription recordfalse
trialingIn free trialtrue
activePaid and currenttrue
past_duePayment failed, in grace periodfalse
cancelled_but_validCancelled but period hasn't endedtrue
pausedSubscription pausedfalse
expiredFully expiredfalse
unknownNetwork failed and no cachefalse

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

FieldTypeDescription
currentOffering | nullThe default offering
allRecord<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

CodeDescription
NETWORK_UNAVAILABLEFell back to cache (not a fatal error)
CACHE_TAMPEREDHMAC verification failed — cache discarded
INVALID_API_KEY401 from API
CUSTOMER_NOT_IDENTIFIEDMust call identify() first
PAYMENT_PROCESSOR_ERRORStripe returned an error
RATE_LIMITED429 from API
EXTENSION_CONTEXT_INVALIDExtension 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.