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...