BillStack Docs

TypeScript SDK

A ready-to-use TypeScript client for calling BillStack APIs from your SaaS application

Overview

While BillStack exposes a standard REST API, wrapping it in a typed client makes integration cleaner. Below is a complete TypeScript SDK you can copy into your project.

Installation

No npm package needed — copy this file into your project:

// lib/billstack.ts

interface BillStackConfig {
  baseUrl: string;
  teamId: string;
  projectId: string;
  apiKey: string;
}

class BillStackClient {
  private base: string;
  private headers: HeadersInit;

  constructor(config: BillStackConfig) {
    this.base = `${config.baseUrl}/api/billstack/teams/${config.teamId}/projects/${config.projectId}`;
    this.headers = {
      'Authorization': `Bearer ${config.apiKey}`,
      'Content-Type': 'application/json',
    };
  }

  private async request<T>(path: string, options?: RequestInit): Promise<T> {
    const res = await fetch(`${this.base}${path}`, {
      ...options,
      headers: { ...this.headers, ...options?.headers },
    });
    if (!res.ok) {
      const body = await res.json().catch(() => ({}));
      throw new BillStackError(res.status, body.error || res.statusText);
    }
    return res.json();
  }

  // ── Customers ───────────────────────────────────────────

  async listCustomers(params?: { limit?: number; offset?: number }) {
    const qs = new URLSearchParams();
    if (params?.limit) qs.set('limit', String(params.limit));
    if (params?.offset) qs.set('offset', String(params.offset));
    return this.request<{ customers: Customer[] }>(`/customers?${qs}`);
  }

  async createCustomer(data: { email: string; name?: string; metadata?: Record<string, string> }) {
    return this.request<{ customer: Customer }>('/customers', {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  async getCustomer(customerId: string) {
    return this.request<{ customer: Customer }>(`/customers/${customerId}`);
  }

  async updateCustomer(customerId: string, data: { email?: string; name?: string; metadata?: Record<string, string> }) {
    return this.request<{ customer: Customer }>(`/customers/${customerId}`, {
      method: 'PATCH',
      body: JSON.stringify(data),
    });
  }

  async deleteCustomer(customerId: string) {
    return this.request<{ success: boolean }>(`/customers/${customerId}`, {
      method: 'DELETE',
    });
  }

  // ── Products ────────────────────────────────────────────

  async listProducts(params?: { activeOnly?: boolean }) {
    const qs = new URLSearchParams();
    if (params?.activeOnly !== undefined) qs.set('activeOnly', String(params.activeOnly));
    return this.request<{ products: Product[] }>(`/products?${qs}`);
  }

  async createProduct(data: { name: string; description?: string; metadata?: Record<string, string> }) {
    return this.request<{ product: Product }>('/products', {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  async getProduct(productId: string) {
    return this.request<{ product: Product }>(`/products/${productId}`);
  }

  async updateProduct(productId: string, data: { name?: string; description?: string; isActive?: boolean; metadata?: Record<string, string> }) {
    return this.request<{ product: Product }>(`/products/${productId}`, {
      method: 'PATCH',
      body: JSON.stringify(data),
    });
  }

  async deleteProduct(productId: string) {
    return this.request<{ success: boolean }>(`/products/${productId}`, {
      method: 'DELETE',
    });
  }

  // ── Prices ──────────────────────────────────────────────

  async listPrices(params?: { activeOnly?: boolean; productId?: string }) {
    const qs = new URLSearchParams();
    if (params?.activeOnly !== undefined) qs.set('activeOnly', String(params.activeOnly));
    if (params?.productId) qs.set('productId', params.productId);
    return this.request<{ prices: Price[] }>(`/prices?${qs}`);
  }

  async createPrice(data: {
    productId: string;
    unitAmount: number;
    currency: string;
    type?: 'recurring' | 'one_time';
    interval?: 'day' | 'week' | 'month' | 'year';
    intervalCount?: number;
    metadata?: Record<string, string>;
  }) {
    return this.request<{ price: Price }>('/prices', {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  async getPrice(priceId: string) {
    return this.request<{ price: Price }>(`/prices/${priceId}`);
  }

  async updatePrice(priceId: string, data: { isActive?: boolean; metadata?: Record<string, string> }) {
    return this.request<{ price: Price }>(`/prices/${priceId}`, {
      method: 'PATCH',
      body: JSON.stringify(data),
    });
  }

  async deletePrice(priceId: string) {
    return this.request<{ success: boolean }>(`/prices/${priceId}`, {
      method: 'DELETE',
    });
  }

  // ── Subscriptions ───────────────────────────────────────

  async listSubscriptions(params?: {
    activeOnly?: boolean;
    customerId?: string;
    limit?: number;
    offset?: number;
  }) {
    const qs = new URLSearchParams();
    if (params?.activeOnly !== undefined) qs.set('activeOnly', String(params.activeOnly));
    if (params?.customerId) qs.set('customerId', params.customerId);
    if (params?.limit) qs.set('limit', String(params.limit));
    if (params?.offset) qs.set('offset', String(params.offset));
    return this.request<{ subscriptions: Subscription[] }>(`/subscriptions?${qs}`);
  }

  async getSubscription(subscriptionId: string) {
    return this.request<{ subscription: Subscription }>(`/subscriptions/${subscriptionId}`);
  }

  async cancelSubscriptionAtPeriodEnd(subscriptionId: string) {
    return this.request<{ subscription: Subscription }>(`/subscriptions/${subscriptionId}`, {
      method: 'PATCH',
      body: JSON.stringify({ cancelAtPeriodEnd: true }),
    });
  }

  async resumeSubscription(subscriptionId: string) {
    return this.request<{ subscription: Subscription }>(`/subscriptions/${subscriptionId}`, {
      method: 'PATCH',
      body: JSON.stringify({ cancelAtPeriodEnd: false }),
    });
  }

  async cancelSubscriptionImmediately(subscriptionId: string) {
    return this.request<{ subscription: Subscription }>(`/subscriptions/${subscriptionId}`, {
      method: 'DELETE',
    });
  }

  // ── Checkout ────────────────────────────────────────────

  async createCheckoutSession(data: {
    customerId: string;
    priceId: string;
    quantity?: number;
    successUrl: string;
    cancelUrl: string;
    mode?: 'subscription' | 'payment';
    trialDays?: number;
    metadata?: Record<string, string>;
  }) {
    return this.request<{ sessionId: string; url: string }>('/checkout', {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  // ── Portal ──────────────────────────────────────────────

  async createPortalSession(data: { customerId: string; ttlMs?: number }) {
    return this.request<{
      sessionId: string;
      token: string;
      portalUrl: string;
      expiresAt: string;
    }>('/portal', {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  // ── Analytics ───────────────────────────────────────────

  async getAnalytics(params?: { growthDays?: number }) {
    const qs = new URLSearchParams();
    if (params?.growthDays) qs.set('growthDays', String(params.growthDays));
    return this.request<Analytics>(`/analytics?${qs}`);
  }

  // ── Referrals ───────────────────────────────────────────

  async getReferralConfig() {
    return this.request<{ config: ReferralConfig }>('/referrals/config');
  }

  async updateReferralConfig(data: {
    enabled: boolean;
    referrerCreditAmount: number;
    refereeCreditAmount: number;
    currency?: string;
  }) {
    return this.request<{ config: ReferralConfig }>('/referrals/config', {
      method: 'PUT',
      body: JSON.stringify(data),
    });
  }

  async listReferralCodes(params?: { limit?: number; offset?: number; activeOnly?: boolean }) {
    const qs = new URLSearchParams();
    if (params?.limit) qs.set('limit', String(params.limit));
    if (params?.offset) qs.set('offset', String(params.offset));
    if (params?.activeOnly !== undefined) qs.set('activeOnly', String(params.activeOnly));
    return this.request<{ codes: ReferralCode[] }>(`/referrals/codes?${qs}`);
  }

  async createReferralCode(data: { customerId: string }) {
    return this.request<{ code: ReferralCode }>('/referrals/codes', {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }

  async getReferralCode(code: string) {
    return this.request<{ code: ReferralCode; referrals: Referral[] }>(`/referrals/codes/${code}`);
  }

  async deactivateReferralCode(code: string) {
    return this.request<{ code: ReferralCode }>(`/referrals/codes/${code}`, {
      method: 'DELETE',
    });
  }

  async applyReferral(data: { code: string; refereeCustomerId: string }) {
    return this.request<{ referral: Referral }>('/referrals/apply', {
      method: 'POST',
      body: JSON.stringify(data),
    });
  }
}

// ── Error Class ─────────────────────────────────────────

class BillStackError extends Error {
  constructor(
    public status: number,
    message: string,
  ) {
    super(`BillStack API error (${status}): ${message}`);
    this.name = 'BillStackError';
  }
}

// ── Type Definitions ────────────────────────────────────

interface Customer {
  id: string;
  email: string;
  name: string | null;
  stripeCustomerId: string;
  metadata: Record<string, string>;
  projectId: string;
  createdAt: string;
  updatedAt: string;
}

interface Product {
  id: string;
  name: string;
  description: string | null;
  isActive: boolean;
  stripeProductId: string;
  metadata: Record<string, string>;
  projectId: string;
  createdAt: string;
}

interface Price {
  id: string;
  productId: string;
  unitAmount: number;
  currency: string;
  type: 'recurring' | 'one_time';
  interval: 'day' | 'week' | 'month' | 'year' | null;
  intervalCount: number | null;
  isActive: boolean;
  stripePriceId: string;
  metadata: Record<string, string>;
  createdAt: string;
}

interface Subscription {
  id: string;
  customerId: string;
  stripeSubscriptionId: string;
  status: 'active' | 'trialing' | 'past_due' | 'canceled' | 'unpaid' | 'incomplete' | 'incomplete_expired' | 'paused';
  priceId: string;
  quantity: number;
  cancelAtPeriodEnd: boolean;
  currentPeriodStart: string;
  currentPeriodEnd: string;
  metadata: Record<string, string>;
  projectId: string;
  createdAt: string;
  updatedAt: string;
}

interface Analytics {
  mrr: number;
  customerCount: number;
  churnRate: number;
  subscriptionBreakdown: Record<string, number>;
  customerGrowth: Array<{ date: string; count: number }>;
  webhookStats: { total: number; processed: number; failed: number };
  referralStats: { totalCodes: number; totalReferrals: number; converted: number };
}

interface ReferralConfig {
  enabled: boolean;
  referrerCreditAmount: number;
  refereeCreditAmount: number;
  currency: string;
  projectId: string;
}

interface ReferralCode {
  code: string;
  customerId: string;
  projectId: string;
  isActive: boolean;
  createdAt: string;
}

interface Referral {
  id: string;
  code: string;
  referrerCustomerId: string;
  refereeCustomerId: string;
  status: 'pending' | 'converted';
  createdAt: string;
  convertedAt: string | null;
}

export { BillStackClient, BillStackError };
export type {
  Customer,
  Product,
  Price,
  Subscription,
  Analytics,
  ReferralConfig,
  ReferralCode,
  Referral,
  BillStackConfig,
};

Usage

import { BillStackClient } from './lib/billstack';

const billstack = new BillStackClient({
  baseUrl: process.env.BILLSTACK_URL!,
  teamId: process.env.BILLSTACK_TEAM_ID!,
  projectId: process.env.BILLSTACK_PROJECT_ID!,
  apiKey: process.env.BILLSTACK_API_KEY!,
});

// Create a customer
const { customer } = await billstack.createCustomer({
  email: 'jane@example.com',
  name: 'Jane Doe',
});

// Create a checkout session
const { url } = await billstack.createCheckoutSession({
  customerId: customer.id,
  priceId: 'pri_xxx',
  successUrl: 'https://myapp.com/success',
  cancelUrl: 'https://myapp.com/pricing',
});

// Generate a portal link
const { portalUrl } = await billstack.createPortalSession({
  customerId: customer.id,
});

// Get analytics
const analytics = await billstack.getAnalytics({ growthDays: 30 });
console.log(`MRR: $${(analytics.mrr / 100).toFixed(2)}`);

Error Handling

import { BillStackClient, BillStackError } from './lib/billstack';

try {
  const { customer } = await billstack.createCustomer({
    email: 'duplicate@example.com',
  });
} catch (error) {
  if (error instanceof BillStackError) {
    console.error(`API error ${error.status}: ${error.message}`);
    // e.g., "BillStack API error (409): Customer with this email already exists"
  }
  throw error;
}

Environment Variables

Add these to your .env file:

BILLSTACK_URL=https://your-billstack.com
BILLSTACK_TEAM_ID=team_abc123
BILLSTACK_PROJECT_ID=proj_abc123
BILLSTACK_API_KEY=bs_a1b2c3d4e5f6...

On this page