Skip to content

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.

Every provider implements the same six methods:

MethodPurpose
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
interface CheckoutParams {
organizationId: string;
planId: string;
priceId: string;
billingEmail: string;
successUrl: string;
cancelUrl: string;
metadata?: Record<string, string>;
}

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;
}

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.

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.