Stripe Integration
Tenantx includes a complete Stripe Checkout + webhook integration for subscription billing.
Architecture
Frontend → POST /subscription/stripe/checkout-session → StripeCheckoutController
└── creates Stripe Checkout Session
└── returns { checkout_url }
Frontend → redirect to checkout_url (Stripe-hosted page)
Stripe → POST /api/stripe/webhook → StripeWebhookController
└── verifies signature
└── dispatches to StripeSubscriptionSyncService
└── updates organization_subscriptions table
Required Environment Variables
STRIPE_KEY=pk_test_... # Publishable key (frontend)
STRIPE_SECRET=sk_test_... # Secret key (backend)
STRIPE_WEBHOOK_SECRET=whsec_... # Webhook signing secret
# One price ID per plan (from your Stripe dashboard)
STRIPE_PRICE_STARTER=price_...
STRIPE_PRICE_PRO=price_...
STRIPE_PRICE_COMPLETE=price_...
STRIPE_PRICE_ENTERPRISE=price_...
Stripe Checkout and the Stripe billing portal should not be used until STRIPE_WEBHOOK_SECRET is set correctly, because the app depends on signed webhooks to activate, renew, and downgrade subscriptions locally.
Local Development with Stripe CLI
- Install the Stripe CLI
- Log in:
stripe login - Forward webhooks to your local server:
stripe listen --forward-to http://localhost:8000/api/stripe/webhook - Copy the webhook signing secret printed by the CLI into
.env:STRIPE_WEBHOOK_SECRET=whsec_<secret from stripe listen --print-secret>
Creating a Test Checkout Session
# Authenticated as an org user, POST to:
curl -X POST http://localhost:8000/api/subscription/stripe/checkout-session \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"plan_slug": "starter", "success_url": "http://localhost:5173/subscription?checkout=success", "cancel_url": "http://localhost:5173/subscription"}'
The response contains { "url": "https://checkout.stripe.com/..." }. Redirect the user there.
Webhook Events Handled
| Event | Action |
|---|---|
customer.subscription.created | Creates or links the local subscription when Stripe creates it before checkout sync finishes |
checkout.session.completed | Creates/activates subscription |
invoice.payment_succeeded | Marks subscription active/renewed |
invoice.payment_failed | Marks subscription in grace period |
customer.subscription.deleted | Marks subscription expired |
customer.subscription.updated | Syncs status, plan, dates |
Idempotency
All webhook events are deduplicated via the stripe_webhook_events table — re-delivery of the same event ID is a no-op.
Smoke Test Checklist
Run this before launch:
- Start Stripe CLI listener (
stripe listen --forward-to ...) - Create a checkout session via the API
- Complete the checkout with Stripe test card
4242 4242 4242 4242 - Verify
organization_subscriptionsrow updated toactive - Trigger a payment failure: use card
4000 0000 0000 0341 - Verify subscription moves to
grace_period - Cancel the subscription in Stripe dashboard
- Verify subscription moves to
expired
Plan Price ID Mapping
Plan price IDs are looked up from environment variables. The lookup key is derived from the plan slug:
// StripeCheckoutController resolves: STRIPE_PRICE_<SLUG_UPPER>
// e.g., plan slug "starter" -> env('STRIPE_PRICE_STARTER')
Ensure each plan row in subscription_plans has a slug that matches an env var.
Production Webhook Configuration
- In Stripe Dashboard → Developers → Webhooks → Add endpoint
- URL:
https://yourdomain.com/api/stripe/webhook - Events to listen to: select all events listed in the table above
- Copy the signing secret into your production
.envasSTRIPE_WEBHOOK_SECRET