@stackbe/sdk
v0.6.0
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,
});
// 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);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' });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 allEnvironment-Aware Redirects
Auto-detect development environment for magic link redirects:
const stackbe = new StackBE({
apiKey,
appId,
devCallbackUrl: 'http://localhost:3000/auth/callback',
});
// In development (NODE_ENV !== 'production'), uses devCallbackUrl automatically
await stackbe.auth.sendMagicLink('[email protected]');
// Redirects to: http://localhost:3000/auth/callback
// In production, uses your configured auth callback URL
// Or pass explicit redirectUrl to override
await stackbe.auth.sendMagicLink('[email protected]', {
redirectUrl: 'https://myapp.com/custom-callback',
});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
