Docs
Stripe Integration

Stripe Integration

Tenantx includes a complete Stripe Checkout + webhook integration for subscription billing.

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

  1. Install the Stripe CLI
  2. Log in: stripe login
  3. Forward webhooks to your local server:
    stripe listen --forward-to http://localhost:8000/api/stripe/webhook
    
  4. 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

EventAction
customer.subscription.createdCreates or links the local subscription when Stripe creates it before checkout sync finishes
checkout.session.completedCreates/activates subscription
invoice.payment_succeededMarks subscription active/renewed
invoice.payment_failedMarks subscription in grace period
customer.subscription.deletedMarks subscription expired
customer.subscription.updatedSyncs 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:

  1. Start Stripe CLI listener (stripe listen --forward-to ...)
  2. Create a checkout session via the API
  3. Complete the checkout with Stripe test card 4242 4242 4242 4242
  4. Verify organization_subscriptions row updated to active
  5. Trigger a payment failure: use card 4000 0000 0000 0341
  6. Verify subscription moves to grace_period
  7. Cancel the subscription in Stripe dashboard
  8. 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

  1. In Stripe Dashboard → Developers → Webhooks → Add endpoint
  2. URL: https://yourdomain.com/api/stripe/webhook
  3. Events to listen to: select all events listed in the table above
  4. Copy the signing secret into your production .env as STRIPE_WEBHOOK_SECRET