Billing Integration Overview
SMTA ships a provider-agnostic billing package (@smta/billing) that lets you swap between Stripe and Lemon Squeezy without changing your application code. All billing logic runs through the BillingProvider interface.
The BillingProvider Interface
Section titled “The BillingProvider Interface”Every provider implements the same six methods:
| Method | Purpose |
|---|---|
createCheckout(params) | Generates a hosted checkout URL and session ID |
handleWebhook(event) | Parses and validates an inbound webhook payload |
getSubscription(id) | Fetches current subscription state from the provider |
cancelSubscription(id) | Schedules a subscription for cancellation |
recordCustomer(orgId, customerId, email, db) | Persists the org-to-provider-customer mapping |
recordSubscriptionUpdate(parsed, db) | Writes subscription status changes to the database |
CheckoutParams
Section titled “CheckoutParams”interface CheckoutParams { organizationId: string; planId: string; priceId: string; billingEmail: string; successUrl: string; cancelUrl: string; metadata?: Record<string, string>;}ParsedWebhookEvent
Section titled “ParsedWebhookEvent”handleWebhook returns a normalized event regardless of provider:
interface ParsedWebhookEvent { type: string; organizationId: string; providerCustomerId: string; providerSubscriptionId: string; plan: string; status: 'active' | 'trialing' | 'past_due' | 'canceled' | 'unpaid'; currentPeriodEnd: Date; cancelAtPeriodEnd: boolean;}Database Tables
Section titled “Database Tables”Both providers write to the same two tables in the platform schema:
platform.billing_customers— maps each organization to its provider-side customer ID and billing email.platform.billing_subscriptions— stores the current subscription status, plan, period end date, and cancellation flag for each organization.
Your application code queries these tables directly; it never needs to call the provider API to check plan status at request time.
Choosing a Provider
Section titled “Choosing a Provider”The provider is selected by which class you instantiate in your application code — it is not a database setting or environment flag. Import the provider you want and pass it wherever your app expects a BillingProvider.
Stripe:
import { StripeProvider } from '@smta/billing/stripe';
const billing = new StripeProvider(process.env.STRIPE_SECRET_KEY!);Lemon Squeezy:
import { LemonSqueezyProvider } from '@smta/billing/lemon-squeezy';
const billing = new LemonSqueezyProvider( process.env.LEMONSQUEEZY_API_KEY!, process.env.LEMONSQUEEZY_STORE_ID!);Once constructed, both objects expose identical methods, so the rest of your code is provider-independent.
Next Steps
Section titled “Next Steps”- Stripe Integration — setup, webhook wiring, and required env vars
- Lemon Squeezy Integration — setup, webhook wiring, and required env vars