Skip to content

SDK reference — @bc-subscriptions/subscriber-sdk

Generated from the canonical integration guide

This page is generated from docs/integration/sdk-reference.md — the single source of truth shared with the public marketing site. Edit the canonical file, then run npm --prefix tools/integration-docs-derive run derive.

Package: @bc-subscriptions/subscriber-sdk Source: packages/subscriber-sdk/ API surface reference: packages/subscriber-sdk/README.md

The subscriber-sdk is a typed TypeScript client for the bc-subscriptions portal API. It's the base layer used by @bc-subscriptions/storefront-catalyst and is available directly for fully custom headless integrations.

The client surface is intentionally lean (established in ADR-0034 §step-2): four namespaces — subscriptions, plans, products, cart — over the subscriber portal API. The SDK does not wrap auth; you obtain a portal session token from the portal auth endpoints (below) and pass it to createClient.


Installation

npm install @bc-subscriptions/subscriber-sdk

Client construction

import { createClient } from '@bc-subscriptions/subscriber-sdk';

const client = createClient({
  token: sessionToken, // portal session JWT (see "Authentication" below) — required
  baseUrl: 'https://subs-api.bigcommerce-testing-7727.workers.dev/api/v1',
  // apiVersion: '2026-06-01',          // optional → sent as the Accept-Version header
  // onDeprecation: (info) => { ... },  // optional → fires on a Deprecation response header
});

createClient(config) returns a typed client object exposing subscriptions, plans, products, and cart. All methods are async and return typed response objects or throw a typed SDKError.

SDKClientConfig:

Field Type Notes
token string Required. Sent as Authorization: Bearer <token> on every request. Use a portal session JWT; for public-only reads (plans, products) any valid token still satisfies the header.
baseUrl string? API base including the version prefix (e.g. …/api/v1).
apiVersion string? Sent as the Accept-Version request header when set.
onDeprecation (info) => void Invoked with { route, sunset, replacement } when a response carries a Deprecation header.

The returned client is immutable — there is no client.token setter. To swap tokens (e.g. after upgrading from an unauthenticated to an authenticated session), construct a new client with the new token.


Authentication

The SDK does not expose auth methods. Obtain a portal session token directly from the portal auth endpoints, then pass it to createClient:

// 1. Request a magic link (unauthenticated)
await fetch(`${API_BASE}/api/v1/portal/auth/request-link`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'shopper@example.com' }),
});
// → email sent with a short-lived link containing a one-time token

// 2. Verify the token from the link URL → portal session JWT
const res = await fetch(`${API_BASE}/api/v1/portal/auth/verify`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ token: tokenFromLinkUrl }),
});
const { token } = await res.json();

// 3. Construct an authenticated client
const client = createClient({ token, baseUrl: `${API_BASE}/api/v1` });

Tokens are short-lived. Store in sessionStorage; do not persist to localStorage. (Catalyst storefronts can use the higher-level createApiClient from @bc-subscriptions/storefront-catalyst, which wraps requestMagicLink / verifyMagicLink — see the Catalyst guide.)


Subscriptions

All subscription methods require an authenticated client. The session token scopes results to the authenticated subscriber — there is no customer_id parameter.

client.subscriptions.list

const page = await client.subscriptions.list({
  cursor: undefined, // optional — opaque cursor from a prior page's next_cursor
  limit: 20,         // optional
});
// Returns: { data: Subscription[]; next_cursor: string | null }

Pagination is cursor-based. Pass the prior page's next_cursor to fetch the next page; a null next_cursor means there are no more pages.

interface Subscription {
  id: string;
  status: string; // 'active' | 'paused' | 'cancelled' | 'past_due' | …
  plan_id: string;
  customer_id: string;
  store_hash: string;
  next_billing_date: string | null; // ISO 8601
  created_at: string;
  updated_at: string;
  [key: string]: unknown; // additional plan/product snapshot fields
}

client.subscriptions.get

const sub = await client.subscriptions.get('sub_abc123'); // positional id
// Returns: Subscription

client.subscriptions.pause / resume

await client.subscriptions.pause('sub_abc123', { idempotencyKey: 'pause-sub_abc123-1' });
await client.subscriptions.resume('sub_abc123');
// Each returns: Subscription

client.subscriptions.cancel

await client.subscriptions.cancel(
  'sub_abc123',
  { reason: 'too_expensive' }, // optional params
  { idempotencyKey: 'cancel-sub_abc123-1' }, // optional
);
// Returns: Subscription (status: 'cancelled')

client.subscriptions.skip / swap

// Skip the next scheduled charge
await client.subscriptions.skip('sub_abc123');

// Swap to a different product variant
await client.subscriptions.swap('sub_abc123', { newVariantId: '9876' });
// Each returns: Subscription

client.subscriptions.updatePaymentMethod

await client.subscriptions.updatePaymentMethod('sub_abc123', {
  paymentMethodId: 'instr_abc', // BC vault instrument id
});
// Returns: Subscription

Payment methods reference BC vault instrument IDs — raw card data is never stored in bc-subscriptions (per ADR-0037).

client.subscriptions.performAction

Generalized action dispatch (US-17.5) for lifecycle actions not covered by a dedicated method:

await client.subscriptions.performAction('sub_abc123', 'reschedule', { next_billing_date: '2026-07-01' });
// Returns: Subscription

All mutating methods accept an optional trailing { idempotencyKey } — sent as the Idempotency-Key header so retries are safe.


Plans

Plans are public-read.

client.plans.list

const page = await client.plans.list({ cursor: undefined, limit: 20 });
// Returns: { data: Plan[]; next_cursor: string | null }
interface Plan {
  id: string;
  name: string;
  store_hash: string;
  status: string;
  billing_frequency: string;
  created_at: string;
  updated_at: string;
  [key: string]: unknown;
}

client.plans.get

const plan = await client.plans.get('plan_abc'); // positional id
// Returns: Plan

Products

Discovery of products that have subscription plans attached.

client.products.listSubscribable

const page = await client.products.listSubscribable({ cursor: undefined, limit: 20 });
// Returns: { data: SubscribableProduct[]; next_cursor: string | null }
interface SubscribableProduct {
  id: string;
  name: string;
  variant_id: string;
  plans: string[]; // plan ids available for this product
  [key: string]: unknown;
}

Cart

Attach a subscription intent to a BC cart before checkout.

client.cart.addSubscriptionItem

const result = await client.cart.addSubscriptionItem(
  'cart_123',
  { plan_id: 'plan_abc', variant_id: '9876', quantity: 1 },
  { idempotencyKey: 'cart_123-plan_abc' }, // optional
);
// Returns:
// {
//   cart_id: string;
//   subscription_preview: { plan_id: string; first_billing_date: string; recurring_price: number };
// }

client.cart.removeSubscriptionItem

await client.cart.removeSubscriptionItem('cart_123', 'item_456');
// Returns: void

Error handling

The SDK throws a typed SDKError — a discriminated union keyed on type (not error classes). Catch and switch on err.type:

import { createClient } from '@bc-subscriptions/subscriber-sdk';
import type { SDKError } from '@bc-subscriptions/subscriber-sdk';

const client = createClient({ token, baseUrl });

try {
  const sub = await client.subscriptions.get('sub_does_not_exist');
} catch (e) {
  const err = e as SDKError;
  switch (err.type) {
    case 'not_found':        // 404 — doesn't exist or not owned by this subscriber
      console.error('Not found');
      break;
    case 'auth_failed':      // 401 — token missing, expired, or invalid → re-auth
      redirectToLogin();
      break;
    case 'forbidden':        // 403
      console.error('Forbidden');
      break;
    case 'conflict':         // 409 — e.g. cancel an already-cancelled subscription
      console.error('Action not valid for current state');
      break;
    case 'validation_failed': // 422 — err.fields lists the offending fields
      console.error('Validation failed:', err.fields);
      break;
    case 'server_error':     // 500
    default:
      throw e;
  }
}

SDKError variants and their HTTP status:

type status extra fields
auth_failed 401
forbidden 403 details?
not_found 404 details?
conflict 409 details?
validation_failed 422 fields: string[]
server_error 500

The typed error model mirrors the API's domain error helpers at apps/api/src/errors/ (see project memory typed-error-pattern).

Retries

The SDK does not retry automatically. Implement retry in your application layer, and prefer passing an idempotencyKey to mutating calls so retries are safe:

async function withRetry<T>(fn: () => Promise<T>, maxAttempts = 3): Promise<T> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (e) {
      const err = e as SDKError;
      // Only retry transient server errors — never 4xx client errors.
      if (attempt === maxAttempts || err.type !== 'server_error') throw e;
      await new Promise((r) => setTimeout(r, 200 * attempt));
    }
  }
  throw new Error('unreachable');
}

const sub = await withRetry(() => client.subscriptions.get('sub_abc123'));

Deprecation signalling

When the API marks a route deprecated, the response carries a Deprecation header (and Sunset / successor Link). Pass an onDeprecation callback to createClient to observe these:

const client = createClient({
  token,
  baseUrl,
  onDeprecation: ({ route, sunset, replacement }) => {
    console.warn(`[subs-sdk] ${route} is deprecated (sunset ${sunset}); use ${replacement}`);
  },
});

Types

Method response interfaces (Subscription, Plan, SubscribableProduct, the *Page cursor wrappers, and the cart types) are exported from the package root:

import type {
  Subscription, SubscriptionPage,
  Plan, PlanPage,
  SubscribableProduct, SubscribableProductPage,
  SubscriptionCartItem, CartSubscriptionResult,
  SDKClient, SDKClientConfig, SDKError,
} from '@bc-subscriptions/subscriber-sdk';

These are currently hand-authored structural interfaces. A generation pipeline against apps/api/openapi.yaml exists (npm run build:typesscripts/build-types.ts, using openapi-typescript); full openapi-driven type generation is tracked in Hive #580.


OpenAPI spec

The full machine-readable API spec is at apps/api/openapi.yaml. Swagger UI is served at https://subs-api.bigcommerce-testing-7727.workers.dev/docs.

Use the spec to: - Verify field shapes before writing client code - Generate mock handlers (e.g. with MSW) for test environments - Validate webhook payloads against the schema