Cosmo Docs
Server

Billing Module

Stripe subscriptions, plans, trials, and webhooks

The Billing module manages all subscription and payment functionality via Stripe. Source: packages/server/src/modules/billing/.

REST Endpoints

All endpoints are prefixed with /api/v1/billing.

MethodPathGuardsDescription
GET/api/v1/billing/statusClerkAuthGuardBilling status for subscription gate
POST/api/v1/billing/trialClerkAuthGuard + @Roles('OWNER', 'ADMIN')Start 14-day Standard trial
POST/api/v1/billing/trial-modal-seenClerkAuthGuardAcknowledge the trial-started modal for the calling user
POST/api/v1/billing/checkoutClerkAuthGuard + @Roles('OWNER', 'ADMIN')Create Stripe Checkout session
POST/api/v1/billing/downgradeClerkAuthGuard + @Roles('OWNER', 'ADMIN')Downgrade to Free plan
POST/api/v1/billing/portalClerkAuthGuard + @Roles('OWNER', 'ADMIN')Create Stripe Customer Portal session
GET/api/v1/billing/subscriptionClerkAuthGuardGet subscription details
GET/api/v1/billing/checkout/successNoneStripe success redirect target
GET/api/v1/billing/checkout/cancelNoneStripe cancel redirect target
POST/api/v1/billing/webhookNoneStripe webhook (signature verified)

Billing Service (services/billing.service.ts)

Plan Lifecycle

NO_PLAN (new org)
    │
    ├──── POST /trial ────▶ TRIALING (14 days, no card)
    │                              │
    │                              ├── (trial ends) ──▶ FREE
    │                              └── POST /checkout ─▶ ACTIVE (paid)
    │
    └──── POST /checkout ─▶ ACTIVE (paid)
                                   │
                                   └── POST /downgrade ─▶ FREE

Nobody starts on FREE directly. NO_PLAN → trial or paid.

getBillingStatus(orgId, userId)

Used by the web app's activation gate. Returns:

{
  hasActivePlan: boolean;      // ACTIVE or TRIALING
  hadPriorSubscription: boolean; // Was ever on a paid plan
  plan: Plan;
  status: SubscriptionStatus;
}

hadPriorSubscription is true when either the org has a subscription record or userId has previously activated a trial (User.trialUsedAt) — which is why the user is a required argument. Without it, deleting an org and creating a new one would offer the trial again.

startTrial(orgId, userId?)

Creates a 14-day trial subscription. No card required. Sets plan to STANDARD with status TRIALING. Calls StripeService.createTrialSubscription(...) with customer, price, and trial period configuration.

Also stamps User.trialUsedAt for userId, so repeat-trial protection survives org deletion. userId is optional for the one caller that has no user in hand — the Clerk organization.created webhook — which instead gets the mark via syncSubscriptionFromStripe's findOrgOwnerUserId lookup. It must be omitted there rather than passed as undefined: Prisma reads an undefined id as "no predicate", so forwarding it would turn markUserTrialUsed's updateMany into an unfiltered write across every user who has never trialled. markUserTrialUsed throws on a falsy id for that reason.

createCheckoutSession(orgId, plan)

Creates a Stripe Checkout session for the specified plan. Returns a url to redirect the user to. On success, Stripe redirects to GET /billing/checkout/success?session_id=....

downgradeToFree(orgId)

  • If currently TRIALING: cancels the trial immediately and sets plan to FREE.
  • If currently ACTIVE (paid): sets cancelAtPeriodEnd: true in Stripe — user retains access until the billing period ends.

createPortalSession(orgId)

Creates a Stripe Customer Portal session URL. Allows users to manage payment methods, view invoices, and cancel subscriptions themselves.

getSubscriptionDetails(orgId)

Returns the subscription details:

{
  plan: Plan;
  status: SubscriptionStatus;
  seats: number;
  purchasedSeats: number | null;
  currentPeriodStart: string | null;  // ISO date string
  currentPeriodEnd: string | null;
  cancelAtPeriodEnd: boolean;
  trialEnd: string | null;
  managedExternally: boolean;         // true for ENTERPRISE/MANUAL
  hasPaymentMethod: boolean;
}

Plans Configuration (plans.config.ts)

PlanDaily Token Limit (per user)Max MembersDescription
NO_PLAN0 (blocked)1Pre-activation
FREE1,000,0001Post-cancellation
STANDARD2,000,0001Default paid plan
PREMIUM10,000,000UnlimitedHigh-usage plan
ENTERPRISE10,000,000UnlimitedCustom contract

Stripe Config (stripe.config.ts)

ConfigDescription
Trial days14
Price IDsConfigured per environment (Standard, Premium)
Webhook secretFor signature verification

Stripe Service (services/stripe.service.ts)

Thin wrapper around the Stripe SDK:

MethodDescription
createCustomer(orgId, email, name)Create Stripe customer
createCheckoutSession(customerId, priceId, metadata)Checkout session
createPortalSession(customerId, returnUrl)Customer portal session
createTrialSubscription(customerId, priceId, trialDays)Trial subscription
cancelSubscription(subscriptionId, immediately?)Cancel subscription
updateSubscription(subscriptionId, params)Update subscription fields

Stripe Webhook Handler (webhooks/stripe-webhook.handler.ts)

Handles Stripe webhook events at POST /api/v1/billing/webhook. Raw body is preserved for Stripe signature verification.

EventAction
checkout.session.completedActivate subscription, set plan to STANDARD/PREMIUM
customer.subscription.updatedSync status changes
customer.subscription.deletedSet plan to FREE
invoice.payment_failedSet status to PAST_DUE
invoice.paidClear PAST_DUE, extend period

Billing Repository (repositories/billing.repository.ts)

All Prisma queries:

  • findSubscription(orgId) → current subscription
  • upsertSubscription(orgId, data) → create or update
  • setPaymentCustomer(orgId, customerId, provider) → store payment customer ID
  • findSubscription(orgId) → current subscription