HyperSaaS
BackendSubscriptions

Checkout & Portal

Stripe Checkout session creation and customer portal integration.

Checkout Flow

1. Create Checkout Session

POST /api/subscriptions/checkout/
{
  "price_id": "price_1234567890",
  "workspace_id": "ws-uuid"
}

The request is refused with 400 unless:

  • the price is an active price of a plan on sale ("That plan isn't on sale."),
  • the workspace is one the user owns or administers ("Choose a workspace you own or administer."), and
  • the workspace has no subscription yet. To change plan, use the billing portal ("This workspace already has a subscription. Change its plan from the billing portal.").

Then the server:

  1. Gets or creates a StripeUser for the authenticated user, and its Stripe customer
  2. Creates a Stripe Checkout Session with the price, naming the workspace in its metadata
  3. Returns the session's ID and Stripe's URL for it
{
  "session_id": "cs_live_...",
  "url": "https://checkout.stripe.com/c/pay/cs_live_..."
}

2. Redirect to Stripe

The frontend sends the customer to the URL; no Stripe.js is needed:

window.location.assign(data.url);

3. Post-Checkout

Stripe returns the customer to the workspace's billing page on the frontend: dashboard/workspaces/{workspace_id}/settings/billing?checkout=success, or ?checkout=canceled if they back out.

Stripe fires webhook events that create the subscription locally and attach it to the workspace named at checkout (see Webhooks). The billing page checks GET /api/workspaces/{id}/plan/ until the subscription appears.

Checkout Parameters

The checkout session is configured with:

{
    "customer": customer_id,
    "success_url": f"{FRONT_END_BASE_URL}/dashboard/workspaces/{workspace_id}/settings/billing?checkout=success&session={{CHECKOUT_SESSION_ID}}",
    "cancel_url": f"{FRONT_END_BASE_URL}/dashboard/workspaces/{workspace_id}/settings/billing?checkout=canceled",
    "payment_method_types": ["card"],
    "mode": "subscription",
    "line_items": [{"price": price_id, "quantity": 1}],
    "subscription_data": {
        "trial_end": trial_end_timestamp,
        "metadata": {"workspace_id": workspace_id}
    },
    "metadata": {"workspace_id": workspace_id},
    "allow_promotion_codes": True
}

Free Trial

If NEW_USER_FREE_TRIAL_DAYS is set (default: 7), a customer's first subscription includes a trial period:

trial_end = now() + timedelta(days=NEW_USER_FREE_TRIAL_DAYS + 1)

The extra day accounts for timezone rounding. Stripe requires the trial end to be at least 48 hours in the future.

Set NEW_USER_FREE_TRIAL_DAYS = None to disable trials.

During the trial, the workspace gets the plan's limits but only $1 of AI credit (TRIAL_AI_CREDIT). The plan's full monthly credit starts when the trial converts.

Customer Portal

The Stripe billing portal lets users manage their subscription without custom UI:

  • View current subscription
  • Change plan (upgrade/downgrade)
  • Update payment method
  • View billing history
  • Cancel subscription

Get Portal URL

POST /api/subscriptions/customer-portal/
{ "workspace_id": "ws-uuid" }
{
  "url": "https://billing.stripe.com/p/session/..."
}

The portal belongs to a Stripe customer: the person who pays. Given a workspace_id, it opens only for whoever pays for that workspace's subscription, and returns to that workspace's billing page. Anyone else gets 403 with who manages billing ("Billing for this workspace is managed by …"); a workspace with no subscription gets 400. Without workspace_id, it opens the caller's own customer and returns to {FRONT_END_BASE_URL}dashboard/.

Configuration

sync_stripe_plans sets the portal's default configuration to offer exactly the plans and prices in plans.py, so customers can switch between them there. Everything else (cancellation policy, proration, branding) is set in the Stripe Dashboard.

Available Prices Endpoint

GET /api/subscriptions/subscribable-product/

Returns every active price of every plan on sale, for the pricing page. No authentication needed. A plan someone already pays for in one workspace is still listed, since they can buy it for another.

Response:

[
  {
    "price_id": "price_123",
    "product_id": "prod_abc",
    "name": "Pro",
    "price": 2000,
    "freq": "month_1",
    "avail": true,
    "currency": "usd",
    "nickname": "Pro monthly",
    "services": []
  }
]

freq is month_1 or year_1.

Configuration

SettingDefaultDescription
STRIPE_API_SECRET—Stripe secret key
FRONT_END_BASE_URL—Frontend URL for redirects
NEW_USER_FREE_TRIAL_DAYS7Trial period length
DEFAULT_PAYMENT_METHOD_TYPES["card"]Accepted payment methods
DEFAULT_CHECKOUT_MODE"subscription"Checkout mode
ALLOW_PROMOTION_CODEStrueEnable promo codes in checkout
CHECKOUT_SUCCESS_URL_PATH"dashboard/workspaces/{workspace_id}/settings/billing"Where checkout returns to; ?checkout=success is added
CHECKOUT_CANCEL_URL_PATH"dashboard/workspaces/{workspace_id}/settings/billing"Where cancelled checkout returns to; ?checkout=canceled is added

On this page