Subscriptions

Overview

This document explains how the frontend integrates with our backend to:

  • List available plans and periods

  • Start a checkout session (Stripe)

  • Confirm payment completion

  • Show current subscription status

  • Manage billing (payment methods, invoices) via Stripe’s Billing Portal

  • List payments

  • Cancel a subscription

Stripe handles payment capture and billing cycles. Our backend:

  • Validates requested plan/period against DB and Stripe configuration

  • Creates Stripe Checkout Sessions

  • Receives Stripe Webhooks to mark payments as completed/failed/expired and record cancellations

  • Exposes REST endpoints for frontend flows

Terminology mapping

  • Subscription (DB) ~ Stripe Product

  • SubscriptionPeriod (DB) ~ Stripe Price

  • OrgSubscription (DB) ~ Logical subscription status per org (we also infer live state from latest payment)

  • Organization (DB) ~ Stripe Customer (linked via stripeCustomerId)

  • Payments (DB) ~ Payment/Invoice/Cancellation events (tracked per Stripe events)

Prerequisites for frontend

  • User must be authenticated; tokens must be sent for protected routes.

  • For paid checkouts:

    • Backend lazily creates a Stripe Customer (first purchase), then reuses it.

    • The backend returns checkoutUrl (direct hosted Checkout URL) and sessionId.

  • External client lib types: use SubscriptionTypes.* DTOs for all request/response typing.

Key backend endpoints

  • GET subscriptions (public): list active plans + periods

  • GET subscriptions/my (auth): current org’s subscription (inferred from last completed payment)

  • POST subscriptions/buy (auth): create a Stripe Checkout Session for a selected period

  • GET subscriptions/checkout-session/:id (auth): get Checkout Session status

  • POST subscriptions/portal (auth): create Stripe Billing Portal session (returns URL)

  • GET subscriptions/payments (auth): list org payments, optional date filters

  • POST subscriptions/cancel (auth): cancel the current Stripe subscription

Data you’ll work with (from client lib)

  • SubscriptionTypes.SubscriptionResponse

  • SubscriptionTypes.SubscriptionPeriodResponse

  • SubscriptionTypes.BuySubscriptionRequest

  • SubscriptionTypes.BuySubscriptionResponse

  • SubscriptionTypes.OrgSubscriptionResponse

  • SubscriptionTypes.MySubscriptionResponse

  • SubscriptionTypes.GetSubscriptionsResponse

  • SubscriptionTypes.PaymentResponse

  • SubscriptionTypes.PaymentsListResponse

  • SubscriptionTypes.PaymentsQueryParams

  • SubscriptionTypes.CancelSubscriptionResponse

  • SubscriptionTypes.CreateBillingPortalResponse

  • SubscriptionTypes.GetCheckoutSessionStatusResponse

End-to-end flows

1) Show available plans (anonymous or authenticated)

  • Call GET subscriptions.

  • Render tier cards: name, description, and available periods with periodType (ALL_TIME, MONTHLY, YEARLY, etc.) and price.

UI notes:

  • Clearly indicate billing interval.

  • Grey-out or badge the active plan if already subscribed (requires checking current subscription separately).

2) Start checkout for selected plan (Hosted Checkout - Redirect)

  • User selects a SubscriptionPeriodResponse.id.

  • Call POST subscriptions/buy with { subscriptionPeriodId }.

  • Response:

    • success: true

    • sessionId (string)

    • checkoutUrl (string) ← Use this to redirect the user to Stripe’s Hosted Checkout

How to redirect (React):

  • On success, immediately navigate the browser to checkoutUrl:

    • window.location.assign(redirectUrl) or window.location.href = redirectUrl

  • No Stripe SDK is required in the frontend. The backend already created the Checkout Session and provides the URL.

Environment notes:

  • The same backend endpoint is used in test and prod; which Stripe environment you hit is determined by the backend’s configured Stripe API key. The redirectUrl returned by the backend always points to the correct environment (test or live). You don’t need to branch logic in the frontend.

UI notes:

  • Handle validation errors (mismatches, active sub, etc.).

  • If the org has an active subscription, you’ll receive a 409 with an informative error_code/message.

3) Confirm purchase completion (client-side)

  • After Hosted Checkout, Stripe redirects the user back to your return URL (FRONTEND_URL + STRIPE_RETURN_PATH) with session_id in the query string.

  • Read session_id from the URL and call GET subscriptions/checkout-session/:id.

  • Expect status, payment_status, and subscriptionId (if recurring).

UI notes:

  • Hosted Checkout always redirects; use the provided checkoutUrl to start checkout.

  • Stripe returns the user to FRONTEND_URL + STRIPE_RETURN_PATH with session_id; read it and confirm via the status endpoint.

  • After checkout completes, poll GET subscriptions/checkout-session/:id 2–3 times over ~5–10 seconds before showing success. Webhook processing may complete milliseconds after the user returns.

4) Show current subscription status

  • Call GET subscriptions/my.

  • Returns either null or OrgSubscriptionResponse with:

    • periodType, price, dateFrom, dateTo (if recurring), status and autoRenew.

UI notes:

  • If status is ACTIVE and dateTo present in the future, show next renewal/expiry.

  • If ALL_TIME, dateTo is null; show “lifetime” badge.

5) Manage billing (payment methods, invoices)

  • Call POST subscriptions/portal.

  • Response: { success, url }.

  • Open url in a new tab to let the user manage card, invoices, and billing info.

UI notes:

  • Portal returns to your FRONTEND_URL when done.

6) View payments

  • Call GET subscriptions/payments?startDate=&endDate=.

  • Response includes PaymentResponse items with status, currency, amount, timestamp.

UI notes:

  • Sort by time desc; show badges for COMPLETED, FAILED, EXPIRED, CANCELLED.

7) Cancel subscription

  • Call POST subscriptions/cancel.

  • Backend validates and cancels the Stripe subscription.

  • You get { success: true } or an error_code if cancellation isn’t allowed (e.g., already canceled).

UI notes:

  • On success, you may still show benefits until dateTo (period-end cancellations).

  • For immediate cancellations, reflect CANCELLED status and remove auto-renew badges.

Error handling

  • All endpoints return { success: false, error_code, message } on error.

  • Common error codes include: USER_NOT_FOUND, NO_ORGANIZATION, SUBSCRIPTION_PERIOD_NOT_FOUND, SUBSCRIPTION_ALREADY_ACTIVE, STRIPE_PRICE_* mismatches, STRIPE_CANCELLATION_FAILED, INVALID_SESSION.

  • Show human-friendly messages derived from message.

Webhooks (FYI)

  • The backend processes Stripe webhooks and:

    • Marks session payments as COMPLETED/FAILED/EXPIRED in Payments

    • Records cancellation events as CANCELLED payments

    • (Planned) Maintains OrgSubscription lifecycle

  • Frontend does not call webhooks. Instead, it:

    • Polls subscriptions/checkout-session/:id post-checkout

    • Calls subscriptions/my to display current status

Typical UI sequence examples

  • New paid subscription:

    1. GET subscriptions

    2. POST subscriptions/buy (with selected subscriptionPeriodId)

    3. Redirect to checkoutUrl

    4. On return (with session_id): GET subscriptions/checkout-session/:id

    5. GET subscriptions/my to show status

  • Manage payment method:

    1. POST subscriptions/portal → open returned url

  • Show billing history:

    1. GET subscriptions/payments (with optional date filters)

  • Cancel:

    1. POST subscriptions/cancel

    2. GET subscriptions/my to refresh the status

Client wrappers (examples)

Assuming your API client has makeRequest<T>(path, method, body):

public getSubscriptions = async (): Promise<SubscriptionTypes.GetSubscriptionsResponse> => {
  return this.makeRequest<SubscriptionTypes.GetSubscriptionsResponse>('subscriptions', 'GET', null);
};

public getMySubscription = async (): Promise<SubscriptionTypes.MySubscriptionResponse> => {
  return this.makeRequest<SubscriptionTypes.MySubscriptionResponse>('subscriptions/my', 'GET', null);
};

public buySubscription = async (data: SubscriptionTypes.BuySubscriptionRequest): Promise<SubscriptionTypes.BuySubscriptionResponse> => {
  return this.makeRequest<SubscriptionTypes.BuySubscriptionResponse>('subscriptions/buy', 'POST', data);
};

public getCheckoutSessionStatus = async (id: string): Promise<GetCheckoutSessionStatusResponse> => {
  return this.makeRequest<GetCheckoutSessionStatusResponse>(`subscriptions/checkout-session/${id}`, 'GET', null);
};

public createBillingPortal = async (): Promise<CreateBillingPortalResponse> => {
  return this.makeRequest<CreateBillingPortalResponse>('subscriptions/portal', 'POST', null);
};

public getPayments = async (params?: SubscriptionTypes.PaymentsQueryParams): Promise<SubscriptionTypes.PaymentsListResponse> => {
  const queryParams = new URLSearchParams();
  if (params?.startDate) queryParams.append('startDate', params.startDate);
  if (params?.endDate) queryParams.append('endDate', params.endDate);
  return this.makeRequest<SubscriptionTypes.PaymentsListResponse>(
    `subscriptions/payments${queryParams.toString() ? `?${queryParams.toString()}` : ''}`,
    'GET',
    null,
  );
};

public cancelSubscription = async (): Promise<SubscriptionTypes.CancelSubscriptionResponse> => {
  return this.makeRequest<SubscriptionTypes.CancelSubscriptionResponse>('subscriptions/cancel', 'POST', null);
};

Testing checklist (sandbox)

  • Plans loaded from GET subscriptions.

  • Checkout succeeds for each period type; Payments show COMPLETED; subscriptions/my reflects status and dates.

  • Failure paths (use Stripe test cards) show FAILED/EXPIRED.

  • Portal opens and returns correctly.

  • Cancellation sets appropriate state; immediate vs period-end behaves as expected.

Notes and caveats

  • Embedded checkout uses ui_mode=embedded and redirect_on_completion=if_required.

  • For recurring periods, metadata is passed so renewals can be tracked reliably.

  • If you ever see an active subscription blocking new purchases, the backend will respond with 409 and an explanatory message.

Last updated

Was this helpful?