@stackbe/sdk
v0.15.4
Published
Official JavaScript/TypeScript SDK for StackBE - the billing backend for your side project
Maintainers
Readme
@stackbe/sdk
Official JavaScript/TypeScript SDK for StackBE - the billing backend for your side project.
Installation
npm install @stackbe/sdkQuick Start
import { StackBE } from '@stackbe/sdk';
const stackbe = new StackBE({
apiKey: process.env.STACKBE_API_KEY!,
appId: process.env.STACKBE_APP_ID!,
});
// Track usage
await stackbe.usage.track('customer_123', 'api_calls');
// Check entitlements
const { hasAccess } = await stackbe.entitlements.check('customer_123', 'premium_export');
// Create checkout session
const { url } = await stackbe.checkout.createSession({
customer: 'cust_123',
planId: 'plan_pro',
successUrl: 'https://myapp.com/success',
});
// Get subscription
const subscription = await stackbe.subscriptions.get('cust_123');
// Send magic link
await stackbe.auth.sendMagicLink('[email protected]');Modules
Usage Tracking
Track billable usage events for your customers:
// Track a single event
await stackbe.usage.track('customer_123', 'api_calls');
// Track multiple units
await stackbe.usage.track('customer_123', 'tokens', { quantity: 1500 });
// Check if within limits
const { allowed, remaining } = await stackbe.usage.check('customer_123', 'api_calls');
if (!allowed) {
throw new Error('Usage limit exceeded');
}
// Get full usage summary
const usage = await stackbe.usage.get('customer_123');
console.log(usage.metrics);
// Track and check in one call
const result = await stackbe.usage.trackAndCheck('customer_123', 'api_calls');
if (!result.allowed) {
// Handle limit exceeded
}Entitlements
Check feature access based on customer's plan:
// Check single feature
const { hasAccess } = await stackbe.entitlements.check('customer_123', 'premium_export');
// Get all entitlements
const { entitlements, planName } = await stackbe.entitlements.getAll('customer_123');
// { premium_export: true, api_access: true, max_projects: 10 }
// Check multiple features at once
const features = await stackbe.entitlements.checkMany('customer_123', [
'premium_export',
'advanced_analytics',
]);
// Require a feature (throws if not available)
await stackbe.entitlements.require('customer_123', 'premium_export');Checkout
Create Stripe checkout sessions:
// With existing customer ID
const { url } = await stackbe.checkout.createSession({
customer: 'cust_123',
planId: 'plan_pro_monthly',
successUrl: 'https://myapp.com/success',
cancelUrl: 'https://myapp.com/pricing',
});
// Redirect to checkout
res.redirect(url);
// With new customer (will be created)
const { url } = await stackbe.checkout.createSession({
customer: { email: '[email protected]', name: 'John' },
planId: 'plan_pro_monthly',
successUrl: 'https://myapp.com/success',
trialDays: 14,
});
// Get checkout URL directly
const checkoutUrl = await stackbe.checkout.getCheckoutUrl({
customer: 'cust_123',
planId: 'plan_pro',
successUrl: 'https://myapp.com/success',
});Subscriptions
Manage customer subscriptions:
// Get current subscription
const subscription = await stackbe.subscriptions.get('cust_123');
if (subscription) {
console.log(`Plan: ${subscription.plan.name}`);
console.log(`Status: ${subscription.status}`);
}
// Check if customer has active subscription
const isActive = await stackbe.subscriptions.isActive('cust_123');
// Cancel subscription (at end of billing period)
await stackbe.subscriptions.cancel('sub_123');
// Cancel immediately
await stackbe.subscriptions.cancel('sub_123', { immediate: true });
// Update subscription (change plan)
await stackbe.subscriptions.update('sub_123', {
planId: 'plan_enterprise',
prorate: true,
});
// Reactivate canceled subscription
await stackbe.subscriptions.reactivate('sub_123');
// List all subscriptions
const subscriptions = await stackbe.subscriptions.list('cust_123');Authentication
Passwordless authentication with magic links:
// Send magic link
await stackbe.auth.sendMagicLink('[email protected]');
// With redirect URL
await stackbe.auth.sendMagicLink('[email protected]', {
redirectUrl: 'https://myapp.com/dashboard',
});
// For localhost development
await stackbe.auth.sendMagicLink('[email protected]', {
useDev: true,
});
// Log user directly into a specific organization
// Customer must already be a member of the org
await stackbe.auth.sendMagicLink('[email protected]', {
organizationId: 'org_abc123',
});
// Verify magic link token (in your /verify route)
const { token } = req.query;
const result = await stackbe.auth.verifyToken(token);
// result includes tenant and org context:
// - customerId, email, sessionToken
// - tenantId (your StackBE tenant)
// - organizationId, orgRole (if in org context)
res.cookie('session', result.sessionToken, { httpOnly: true });
res.redirect('/dashboard');
// Get session from token (includes tenant context)
const session = await stackbe.auth.getSession(sessionToken);
if (session) {
console.log(session.customerId);
console.log(session.email);
console.log(session.tenantId); // Tenant context
console.log(session.organizationId); // Org context (if applicable)
console.log(session.subscription);
console.log(session.entitlements);
}
// Check if authenticated
const isAuthenticated = await stackbe.auth.isAuthenticated(sessionToken);
// Switch to a different organization (for multi-org users)
const orgs = await stackbe.organizations.listByCustomer(customerId);
if (orgs.length > 1) {
// User belongs to multiple orgs - show org picker
const { sessionToken: newToken, organizationId, orgRole } =
await stackbe.auth.switchOrganization(sessionToken, targetOrgId);
// Store newToken - it's scoped to the target org
}Plans & Products
List available pricing plans for your pricing page:
// List all plans
const plans = await stackbe.plans.list();
// List active plans sorted by price
const plans = await stackbe.plans.listByPrice();
// [Free ($0), Starter ($9), Pro ($29), Enterprise ($99)]
// Filter by product
const plans = await stackbe.plans.list({ productId: 'prod_123' });
// Get a specific plan
const plan = await stackbe.plans.get('plan_123');
console.log(plan.name, plan.priceCents, plan.entitlements);
// List products
const products = await stackbe.products.list();
// Get product details
const product = await stackbe.products.get('prod_123');Dynamic Pricing Page Example
// Next.js pricing page
export default async function PricingPage() {
const plans = await stackbe.plans.listByPrice();
return (
<div className="grid grid-cols-3 gap-4">
{plans.map((plan) => (
<div key={plan.id} className="border p-4 rounded">
<h3>{plan.name}</h3>
<p className="text-2xl">
${plan.priceCents / 100}/{plan.interval}
</p>
<ul>
{Object.entries(plan.entitlements).map(([key, value]) => (
<li key={key}>
{key}: {value === true ? '✓' : value}
</li>
))}
</ul>
<a href={`/checkout?plan=${plan.id}`}>
{plan.priceCents === 0 ? 'Start Free' : 'Subscribe'}
</a>
</div>
))}
</div>
);
}Customer Management
// Get customer
const customer = await stackbe.customers.get('cust_123');
// Get by email
const customer = await stackbe.customers.getByEmail('[email protected]');
// Create customer
const newCustomer = await stackbe.customers.create({
email: '[email protected]',
name: 'John Doe',
metadata: { source: 'api' },
});
// Get or create (idempotent)
const customer = await stackbe.customers.getOrCreate({
email: '[email protected]',
name: 'John Doe',
});
// Update customer
await stackbe.customers.update('cust_123', { name: 'Jane Doe' });Organizations (B2B Multi-User)
Manage customer organizations for B2B apps:
// Create organization with customer as owner
const org = await stackbe.organizations.create({
name: 'Acme Corp',
ownerId: customer.id,
});
// List all organizations
const orgs = await stackbe.organizations.list();
// List organizations for a specific customer (for org switchers)
const customerOrgs = await stackbe.organizations.listByCustomer('cust_123');
// Get organization by ID
const org = await stackbe.organizations.get('org_123');
// Update organization
await stackbe.organizations.update('org_123', { name: 'New Name' });
// Delete organization (must have no active subscriptions)
await stackbe.organizations.delete('org_123');
// Add member to organization
await stackbe.organizations.addMember('org_123', {
customerId: 'cust_456',
role: 'member', // 'admin' | 'member'
});
// Remove member
await stackbe.organizations.removeMember('org_123', 'member_456');
// Update member role
await stackbe.organizations.updateMember('org_123', 'member_456', {
role: 'admin',
});
// Invite by email
await stackbe.organizations.invite('org_123', {
email: '[email protected]',
role: 'member',
});
// List pending invites
const invites = await stackbe.organizations.listInvites('org_123');
// Cancel invite
await stackbe.organizations.cancelInvite('org_123', 'invite_456');B2B Signup Flow
async function signup(email: string, orgName: string) {
// 1. Get or create customer
const customer = await stackbe.customers.getOrCreate({ email });
// 2. Create organization with customer as owner
const org = await stackbe.organizations.create({
name: orgName,
ownerId: customer.id,
});
// 3. Send magic link
await stackbe.auth.sendMagicLink(email);
return { customer, org };
}Express Middleware
Track Usage Automatically
app.use(stackbe.middleware({
getCustomerId: (req) => req.user?.customerId,
metric: 'api_calls',
skip: (req) => req.path === '/health',
}));Require Feature Entitlements
app.get('/api/export',
stackbe.requireFeature({
getCustomerId: (req) => req.user?.customerId,
feature: 'premium_export',
onDenied: (req, res) => {
res.status(403).json({ error: 'Upgrade to Pro' });
},
}),
exportHandler
);Enforce Usage Limits
app.use('/api',
stackbe.enforceLimit({
getCustomerId: (req) => req.user?.customerId,
metric: 'api_calls',
onLimitExceeded: (req, res, { current, limit }) => {
res.status(429).json({ error: 'Rate limit exceeded', current, limit });
},
})
);Authenticate Requests
app.use('/dashboard',
stackbe.auth.middleware({
getToken: (req) => req.cookies.session,
onUnauthenticated: (req, res) => res.redirect('/login'),
})
);
app.get('/dashboard', (req, res) => {
// req.customer, req.subscription, req.entitlements are available
res.json({ email: req.customer.email });
});Next.js Integration
API Routes (App Router)
// app/api/generate/route.ts
import { StackBE } from '@stackbe/sdk';
import { NextResponse } from 'next/server';
const stackbe = new StackBE({
apiKey: process.env.STACKBE_API_KEY!,
appId: process.env.STACKBE_APP_ID!,
});
export async function POST(request: Request) {
const { customerId } = await request.json();
// Check limits
const { allowed, remaining } = await stackbe.usage.check(customerId, 'generations');
if (!allowed) {
return NextResponse.json({ error: 'Limit reached' }, { status: 429 });
}
// Track usage
await stackbe.usage.track(customerId, 'generations');
// Do work...
return NextResponse.json({ success: true, remaining: remaining! - 1 });
}Server Actions
'use server';
import { StackBE } from '@stackbe/sdk';
const stackbe = new StackBE({
apiKey: process.env.STACKBE_API_KEY!,
appId: process.env.STACKBE_APP_ID!,
});
export async function exportData(customerId: string) {
const { hasAccess } = await stackbe.entitlements.check(customerId, 'data_export');
if (!hasAccess) {
throw new Error('Upgrade to Pro to export data');
}
// Perform export...
}Error Handling
The SDK provides typed error codes for specific error handling:
import { StackBE, StackBEError } from '@stackbe/sdk';
try {
await stackbe.auth.verifyToken(token);
} catch (error) {
if (error instanceof StackBEError) {
// Handle specific error types
switch (error.code) {
case 'TOKEN_EXPIRED':
return res.redirect('/login?error=expired');
case 'TOKEN_ALREADY_USED':
return res.redirect('/login?error=used');
case 'SESSION_EXPIRED':
return res.redirect('/login');
case 'CUSTOMER_NOT_FOUND':
return res.status(404).json({ error: 'Customer not found' });
case 'USAGE_LIMIT_EXCEEDED':
return res.status(429).json({ error: 'Limit exceeded' });
default:
return res.status(500).json({ error: 'Something went wrong' });
}
// Or use helper methods
if (error.isAuthError()) {
return res.redirect('/login');
}
if (error.isNotFoundError()) {
return res.status(404).json({ error: error.message });
}
}
}Error Codes
| Category | Codes |
|----------|-------|
| Auth | TOKEN_EXPIRED, TOKEN_ALREADY_USED, TOKEN_INVALID, SESSION_EXPIRED, SESSION_INVALID, UNAUTHORIZED |
| Resources | NOT_FOUND, CUSTOMER_NOT_FOUND, SUBSCRIPTION_NOT_FOUND, PLAN_NOT_FOUND, APP_NOT_FOUND |
| Usage | USAGE_LIMIT_EXCEEDED, METRIC_NOT_FOUND |
| Entitlements | FEATURE_NOT_AVAILABLE, NO_ACTIVE_SUBSCRIPTION |
| Validation | VALIDATION_ERROR, MISSING_REQUIRED_FIELD, INVALID_EMAIL |
| Network | TIMEOUT, NETWORK_ERROR, UNKNOWN_ERROR |
Configuration
const stackbe = new StackBE({
apiKey: 'sk_live_...', // Required: Your API key
appId: 'app_...', // Required: Your App ID
baseUrl: 'https://api.stackbe.io', // Optional: API base URL
timeout: 30000, // Optional: Request timeout in ms
// Session caching (reduces API calls)
sessionCacheTTL: 120, // Optional: Cache sessions for 2 minutes
// Environment-aware redirects
devCallbackUrl: 'http://localhost:3000/auth/callback', // Optional: Auto-use in dev
});Session Caching
Enable session caching to reduce API calls on every request:
const stackbe = new StackBE({
apiKey,
appId,
sessionCacheTTL: 120, // Cache for 2 minutes
});
// Cached calls won't hit the API
const session1 = await stackbe.auth.getSession(token); // API call
const session2 = await stackbe.auth.getSession(token); // From cache
// Manually invalidate cache
stackbe.auth.invalidateSession(token);
stackbe.auth.clearCache(); // Clear allMagic Link Callback URLs
Recommended approach: Pass the callback URL explicitly for maximum reliability:
// Determine callback URL based on your environment
const callbackUrl = process.env.NODE_ENV === 'production'
? 'https://myapp.com/auth/callback'
: 'http://localhost:3000/auth/callback';
await stackbe.auth.sendMagicLink('[email protected]', {
redirectUrl: callbackUrl,
});Alternative: Use devCallbackUrl for automatic detection (uses devCallbackUrl when NODE_ENV !== 'production'):
const stackbe = new StackBE({
apiKey,
appId,
devCallbackUrl: 'http://localhost:3000/auth/callback',
});
// In development, uses devCallbackUrl automatically
await stackbe.auth.sendMagicLink('[email protected]');
// In production, falls back to app settings callback URL
// Explicit redirectUrl always takes priority if providedNotes:
devCallbackUrlonly activates whenNODE_ENV !== 'production'- Explicit
redirectUrlparameter always takes priority - For predictable behavior, use the explicit approach above
Webhooks
Typed webhook payloads for handling StackBE events:
import type {
AnyWebhookEvent,
SubscriptionCreatedEvent,
SubscriptionCancelledEvent,
PaymentFailedEvent,
} from '@stackbe/sdk';
// In your webhook handler
app.post('/webhooks/stackbe', (req, res) => {
const event = req.body as AnyWebhookEvent;
switch (event.type) {
case 'subscription_created':
const sub = event as SubscriptionCreatedEvent;
console.log(`New subscription: ${sub.data.planName}`);
break;
case 'subscription_cancelled':
const cancelled = event as SubscriptionCancelledEvent;
console.log(`Cancelled: ${cancelled.data.id}`);
break;
case 'payment_failed':
const payment = event as PaymentFailedEvent;
console.log(`Payment failed: ${payment.data.failureReason}`);
// Send dunning email
break;
}
res.json({ received: true });
});Webhook Event Types
| Event | Payload |
|-------|---------|
| subscription_created | SubscriptionWebhookPayload |
| subscription_updated | SubscriptionWebhookPayload |
| subscription_cancelled | SubscriptionWebhookPayload |
| subscription_renewed | SubscriptionWebhookPayload |
| trial_started | SubscriptionWebhookPayload |
| trial_ended | SubscriptionWebhookPayload |
| payment_succeeded | PaymentWebhookPayload |
| payment_failed | PaymentWebhookPayload |
| customer_created | CustomerWebhookPayload |
| customer_updated | CustomerWebhookPayload |
TypeScript
Full type definitions included:
import type {
Customer,
Subscription,
SubscriptionWithPlan,
CheckoutSessionResponse,
SessionResponse,
TrackUsageResponse,
CheckEntitlementResponse,
StackBEErrorCode,
WebhookEventType,
AnyWebhookEvent,
} from '@stackbe/sdk';License
MIT
