Billing & Subscriptions
TendSocial uses Lemon Squeezy as the payment processor for subscription billing.
Architecture
Frontend → POST /api/billing/checkout → Lemon Squeezy → Webhook → Update subscriptionAPI Endpoints
POST /api/billing/checkout
Creates a checkout URL for starting a subscription.
typescript
// Request
{
plan?: string, // e.g., 'professional', 'starter'
variantId?: string, // Direct Lemon Squeezy variant ID (overrides plan)
redirectUrl: string // Where to redirect after checkout
}
// Response
{ url: string } // Lemon Squeezy checkout URLFlow:
- Frontend calls checkout with plan name
- Backend maps plan → variantId via
ConfigService.getBillingConfig() - Creates checkout URL with user/company metadata
- User completes payment on Lemon Squeezy
- Webhook updates subscription status
GET /api/billing/subscription
Returns current subscription details for the company.
typescript
// Response
{
id: string,
status: 'active' | 'cancelled' | 'expired' | 'past_due',
planName: string,
currentPeriodEnd: Date,
cancelAtPeriodEnd: boolean
}POST /api/billing/portal
Returns a customer portal URL for managing subscription.
typescript
// Response
{ url: string } // Lemon Squeezy customer portalWebhook Handling
Endpoint: POST /api/billing/webhook (public, no auth)
Webhook signature is verified using LEMONSQUEEZY_WEBHOOK_SECRET.
Events handled:
subscription_created- New subscription, update company statussubscription_updated- Plan change, status changesubscription_cancelled- Mark as cancelledsubscription_payment_failed- Mark as past_due
Configuration
Required environment variables:
LEMONSQUEEZY_API_KEY- API accessLEMONSQUEEZY_STORE_ID- Store identifierLEMONSQUEEZY_WEBHOOK_SECRET- Webhook signature verification
Runtime config (via ConfigService):
billingConfig.storeId- Override store IDbillingConfig.variantMapping- Map plan names to variant IDs
Database Schema
prisma
model Company {
subscriptionId String?
subscriptionStatus String?
subscriptionPlanName String?
subscriptionExpiresAt DateTime?
}Testing
- Use Lemon Squeezy test mode
- Test webhook with
ngrokor similar tunnel - Mock webhook events for integration tests