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 ProductSubscriptionPeriod(DB) ~ Stripe PriceOrgSubscription(DB) ~ Logical subscription status per org (we also infer live state from latest payment)Organization(DB) ~ Stripe Customer (linked viastripeCustomerId)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) andsessionId.
External client lib types: use
SubscriptionTypes.*DTOs for all request/response typing.
Key backend endpoints
GET
subscriptions(public): list active plans + periodsGET
subscriptions/my(auth): current org’s subscription (inferred from last completed payment)POST
subscriptions/buy(auth): create a Stripe Checkout Session for a selected periodGET
subscriptions/checkout-session/:id(auth): get Checkout Session statusPOST
subscriptions/portal(auth): create Stripe Billing Portal session (returns URL)GET
subscriptions/payments(auth): list org payments, optional date filtersPOST
subscriptions/cancel(auth): cancel the current Stripe subscription
Data you’ll work with (from client lib)
SubscriptionTypes.SubscriptionResponseSubscriptionTypes.SubscriptionPeriodResponseSubscriptionTypes.BuySubscriptionRequestSubscriptionTypes.BuySubscriptionResponseSubscriptionTypes.OrgSubscriptionResponseSubscriptionTypes.MySubscriptionResponseSubscriptionTypes.GetSubscriptionsResponseSubscriptionTypes.PaymentResponseSubscriptionTypes.PaymentsListResponseSubscriptionTypes.PaymentsQueryParamsSubscriptionTypes.CancelSubscriptionResponseSubscriptionTypes.CreateBillingPortalResponseSubscriptionTypes.GetCheckoutSessionStatusResponse
End-to-end flows
1) Show available plans (anonymous or authenticated)
Call GET
subscriptions.Render tier cards: name, description, and available
periodswithperiodType(ALL_TIME, MONTHLY, YEARLY, etc.) andprice.
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/buywith{ subscriptionPeriodId }.Response:
success: truesessionId(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)orwindow.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
redirectUrlreturned 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) withsession_idin the query string.Read
session_idfrom the URL and call GETsubscriptions/checkout-session/:id.Expect
status,payment_status, andsubscriptionId(if recurring).
UI notes:
Hosted Checkout always redirects; use the provided
checkoutUrlto start checkout.Stripe returns the user to
FRONTEND_URL + STRIPE_RETURN_PATHwithsession_id; read it and confirm via the status endpoint.After checkout completes, poll GET
subscriptions/checkout-session/:id2–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
OrgSubscriptionResponsewith:periodType,price,dateFrom,dateTo(if recurring),statusandautoRenew.
UI notes:
If
statusis ACTIVE anddateTopresent in the future, show next renewal/expiry.If
ALL_TIME,dateTois null; show “lifetime” badge.
5) Manage billing (payment methods, invoices)
Call POST
subscriptions/portal.Response:
{ success, url }.Open
urlin a new tab to let the user manage card, invoices, and billing info.
UI notes:
Portal returns to your
FRONTEND_URLwhen done.
6) View payments
Call GET
subscriptions/payments?startDate=&endDate=.Response includes
PaymentResponseitems withstatus,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
CANCELLEDstatus 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
PaymentsRecords cancellation events as CANCELLED payments
(Planned) Maintains
OrgSubscriptionlifecycle
Frontend does not call webhooks. Instead, it:
Polls
subscriptions/checkout-session/:idpost-checkoutCalls
subscriptions/myto display current status
Typical UI sequence examples
New paid subscription:
GET
subscriptionsPOST
subscriptions/buy(with selectedsubscriptionPeriodId)Redirect to
checkoutUrlOn return (with
session_id): GETsubscriptions/checkout-session/:idGET
subscriptions/myto show status
Manage payment method:
POST
subscriptions/portal→ open returnedurl
Show billing history:
GET
subscriptions/payments(with optional date filters)
Cancel:
POST
subscriptions/cancelGET
subscriptions/myto 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;
Paymentsshow COMPLETED;subscriptions/myreflects 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=embeddedandredirect_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?