Overview
The React SDK provides components, hooks, and context for managing billing and subscriptions in your React applications. It works with any React framework and requires a backend server running the Flowglad Server SDK.
Installation
npm install @flowglad/react @flowglad/server
Note: This package requires @flowglad/server to be set up on your backend. See the Server tab or Server SDK documentation for setup instructions.
Quick Start
1. Wrap Your App with FlowgladProvider
import { FlowgladProvider } from '@flowglad/react'
export default function App({ children }) {
return (
<FlowgladProvider
requestConfig={{
headers: {
// Add custom headers if needed
},
}}
>
{children}
</FlowgladProvider>
)
}
2. Use the useBilling Hook
import { useBilling } from '@flowglad/react'
export default function BillingPage() {
const { checkFeatureAccess, customer, paymentMethods } = useBilling()
if (!checkFeatureAccess) {
return <div>Loading...</div>
}
if (checkFeatureAccess('premium_feature')) {
return <div>You have access!</div>
}
return <div>Please upgrade</div>
}
Key Features
- Type-safe Hooks: Access billing data with full TypeScript support
- React Context: Global state management for billing information
- Feature Access Control: Check user access to features
- Checkout Sessions: Create and manage payment flows
- Subscription Management: Handle subscription lifecycles
API Reference
FlowgladProvider
The main provider component that wraps your application. See FlowgladProvider.tsx for more details
Props
interface FlowgladProviderProps {
requestConfig?: {
headers?: Record<string, string>
}
baseURL?: string
betterAuthBasePath?: string
children: React.ReactNode
}
Example
<FlowgladProvider
requestConfig={{
headers: {
'X-Custom-Header': 'value',
},
}}
>
{children}
</FlowgladProvider>
useBilling Hook
Access billing data and functions throughout your application.
Load State Helpers
The hook surfaces billing lifecycle metadata you can use to gate UI:
loaded: boolean – true once billing data has settled (success or error).
errors: Error[] | null – Populated when the last fetch failed.
reload: () => Promise<void> – Invalidates and refetches billing data.
const { loaded, errors, reload } = useBilling()
if (!loaded) return <p>Loading billing…</p>
if (errors?.length) {
return (
<div>
<p>Unable to load billing information.</p>
<button onClick={reload}>Try again</button>
</div>
)
}
Return Value
interface BillingContext {
// Customer data
customer: Customer | null
// Payment methods
paymentMethods: PaymentMethod[]
// Subscriptions
currentSubscription: Subscription // The most recently created current subscription for the customer
currentSubscriptions: Subscription[] // Subscriptions currently active or pending. Length will always be 1 unless your organization settings allow for multiple subscriptions per customer.
subscriptions: Subscription[] // All subscriptions for the customer
// Purchases
purchases: Purchase[]
// Invoices
invoices: Invoice[]
// Catalog and pricing
pricingModel: PricingModel // The customer's pricing model
billingPortalUrl: string // URL to the customer's billing portal
// Feature access
checkFeatureAccess: (featureSlug: string) => boolean
checkUsageBalance: (
usageMeterSlug: string,
options?: { subscriptionId?: string }
) => { availableBalance: number } | null
getProduct: (productSlug: string) => CatalogProduct | null
getPrice: (priceSlug: string) => Price | null
// Checkout
createCheckoutSession: (params: CheckoutSessionParams) => Promise<void>
createAddPaymentMethodCheckoutSession: (
params: AddPaymentMethodCheckoutParams
) => Promise<void>
createActivateSubscriptionCheckoutSession: (
params: ActivateSubscriptionCheckoutParams
) => Promise<void>
// Subscription management
cancelSubscription: (params: CancelSubscriptionParams) => Promise<void>
uncancelSubscription: (params: UncancelSubscriptionParams) => Promise<void>
// Usage tracking
createUsageEvent: (params: CreateUsageEventParams) => Promise<UsageEventResult>
// Refresh data
reload: () => Promise<void>
loaded: boolean
errors: Error[] | null
}
Example Usage
import { useBilling } from '@flowglad/react'
function FeatureGate({ featureSlug, children }) {
const { checkFeatureAccess, createCheckoutSession } = useBilling()
if (!checkFeatureAccess) {
return <div>Loading...</div>
}
if (!checkFeatureAccess(featureSlug)) {
return (
<div>
<p>Upgrade to access this feature</p>
<button
onClick={() =>
createCheckoutSession({
priceSlug: 'pro_plan',
successUrl: window.location.href,
cancelUrl: window.location.href,
autoRedirect: true,
})
}
>
Upgrade Now
</button>
</div>
)
}
return <>{children}</>
}
usePricing Hook
Fetch public pricing data without requiring authentication. Use this for pricing pages and plan cards where you only need the pricing model.
import { useBilling, usePricing } from '@flowglad/react'
function PricingCards() {
const pricingModel = usePricing()
const { createCheckoutSession } = useBilling()
if (!pricingModel) {
return <div>Loading pricing...</div>
}
return (
<div>
{pricingModel.products.map((product) => {
const defaultPrice = product.defaultPrice ?? product.prices?.[0]
if (!defaultPrice) return null
return (
<article key={product.id}>
<h3>{product.name}</h3>
<p>{product.description}</p>
<button
onClick={() =>
createCheckoutSession({
priceSlug: defaultPrice.slug,
successUrl: window.location.href,
cancelUrl: window.location.href,
autoRedirect: true,
})
}
>
Choose {defaultPrice.name ?? product.name}
</button>
</article>
)
})}
</div>
)
}
createCheckoutSession
Create a new checkout session for subscriptions or one-time payments.
Checkout sessions for specific prices can be made using either priceSlug (an identifier that you define), or priceId (an identifier Flowglad defines) as a parameter.
priceSlug is recommended, so that you don’t need to store any references to Flowglad ids in your application database or environment variables.
Parameters
type FrontendProductCreateCheckoutSessionParams =
{
/**
* Must specify either priceSlug or priceId
* Slug is encouraged over Id
*/
priceSlug?: string
priceId?: string
successUrl: string
cancelUrl: string
/**
* Whether to automatically redirect to the hosted checkout page after the session is created
*/
autoRedirect?: boolean
quantity?: number
/**
* the metadata values to set on the object created by this checkout session
* - subscription.metadata for subscription prices
* - purchase.metadata for single payment prices
*/
outputMetadata?: Record<string, any>
/**
* the name value to set on the object created by this checkout session
* - subscription.name for subscription prices
* - purchase.name for single payment prices
*/
outputName?: string
}
Example
const handleUpgrade = async () => {
await createCheckoutSession({
priceSlug: 'price_premium_monthly',
successUrl: `${window.location.origin}/billing/success`,
cancelUrl: `${window.location.origin}/billing`,
autoRedirect: true,
})
}
createAddPaymentMethodCheckoutSession
Open a Flowglad-hosted flow that collects and stores a new payment method for the signed-in customer.
Parameters
type FrontendCreateAddPaymentMethodCheckoutSessionParams = {
successUrl: string
cancelUrl: string
autoRedirect?: boolean
outputMetadata?: Record<string, any>
outputName?: string
/**
* When provided, Flowglad sets the newly created payment method
* as the default for the given subscription.
*/
targetSubscriptionId?: string
}
Example
await createAddPaymentMethodCheckoutSession({
successUrl: `${window.location.origin}/billing/payment-methods`,
cancelUrl: window.location.href,
autoRedirect: true,
})
createActivateSubscriptionCheckoutSession
Trigger the activation flow for an existing subscription that needs a payment method before billing can start. Activations can happen for subscriptions on trial, or subscriptions where payment is due but has not yet been completed.
Parameters
type FrontendCreateActivateSubscriptionCheckoutSessionParams = {
/**
* The subscription to activate.
*/
targetSubscriptionId: string
successUrl: string
cancelUrl: string
autoRedirect?: boolean
outputMetadata?: Record<string, any>
outputName?: string
}
Example
await createActivateSubscriptionCheckoutSession({
targetSubscriptionId: 'sub_123',
successUrl: `${window.location.origin}/billing/success`,
cancelUrl: window.location.href,
autoRedirect: true,
})
cancelSubscription
Cancel a subscription owned by the current customer and refresh billing data.
Parameters
interface CancelSubscriptionParams {
id: string
cancellation:
| { timing: 'at_end_of_current_billing_period' }
| { timing: 'immediately' }
}
Example
await cancelSubscription({
id: 'sub_123',
cancellation: {
timing: 'at_end_of_current_billing_period',
},
})
uncancelSubscription
Reverse a scheduled subscription cancellation. The subscription must be in cancellation_scheduled status. Automatically refreshes billing data on success.
Parameters
interface UncancelSubscriptionParams {
id: string
}
Example
import { useBilling } from '@flowglad/react'
import { useState } from 'react'
function SubscriptionManager() {
const { uncancelSubscription, currentSubscription } = useBilling()
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const subscription = currentSubscription
const isScheduledForCancellation = subscription?.status === 'cancellation_scheduled'
const handleUncancel = async () => {
if (!subscription?.id) return
setIsLoading(true)
setError(null)
try {
await uncancelSubscription({ id: subscription.id })
// Billing data is automatically refreshed
} catch (err) {
setError(
err instanceof Error
? err.message
: 'Failed to uncancel subscription'
)
} finally {
setIsLoading(false)
}
}
if (!isScheduledForCancellation) {
return null
}
return (
<div>
<p>Your subscription is scheduled to cancel on {new Date(subscription.cancelScheduledAt).toLocaleDateString()}</p>
<button onClick={handleUncancel} disabled={isLoading}>
{isLoading ? 'Restoring...' : 'Keep My Subscription'}
</button>
{error && <p className="error">{error}</p>}
</div>
)
}
Behavior:
- Operation is idempotent. If the subscription is not scheduled to cancel, the method will silently succeed without uncanceling
- For paid subscriptions: requires an active payment method for the subscription
- Automatically refreshes billing data after success
createUsageEvent
Record a usage event for usage-based billing directly from the client. This is useful for tracking consumption in real-time as users interact with your application.
Parameters
type CreateUsageEventParams = {
/**
* Identifier for the usage price or meter (exactly one required)
*/
priceSlug?: string
priceId?: string
usageMeterSlug?: string
usageMeterId?: string
/**
* The quantity of usage to record.
* Defaults to 1 if not provided.
*/
amount?: number
/**
* The subscription to record usage against.
* If not provided, uses the customer's current subscription.
*/
subscriptionId?: string
/**
* A unique identifier for this usage event.
* Auto-generated if not provided. Use for idempotency.
*/
transactionId?: string
/**
* Timestamp (in milliseconds) of when the usage occurred.
* Defaults to now if not provided.
*/
usageDate?: number
/**
* Additional properties for the usage event.
* Required for 'count_distinct_properties' aggregation type.
*/
properties?: Record<string, unknown>
}
Returns
// On success
{ usageEvent: { id: string } }
// On error
{ error: { code: string; json: Record<string, unknown> } }
Example
import { useBilling } from '@flowglad/react'
function AIChat() {
const { createUsageEvent } = useBilling()
const handleSendMessage = async (message: string) => {
// Process the message and get token count
const response = await sendToAI(message)
const tokensUsed = response.tokensConsumed
// Record the usage
const result = await createUsageEvent({
usageMeterSlug: 'ai_tokens',
amount: tokensUsed,
})
if ('error' in result) {
console.error('Failed to record usage:', result.error)
}
}
return (
// Chat UI...
)
}
Smart Defaults
The client-side createUsageEvent provides convenient defaults:
amount: Defaults to 1 if not provided
subscriptionId: Auto-inferred from the customer’s current subscription
transactionId: Auto-generated for idempotency if not provided
This means for simple use cases, you only need to provide the price or usage meter identifier:
// Minimal usage - just specify what you're tracking
await createUsageEvent({ priceSlug: 'api_calls' })
// With custom amount
await createUsageEvent({ usageMeterSlug: 'tokens', amount: 150 })
Usage events do not automatically refresh billing data to avoid performance issues with high-frequency events. Call reload() manually if you need to update the displayed usage balance.
checkFeatureAccess
Check if the current customer has access to a specific feature.
Parameters
featureSlug: string - The slug of the feature to check
Returns
boolean - true if the customer has access, false otherwise
Example
const hasAdvancedAnalytics = checkFeatureAccess('advanced_analytics')
if (hasAdvancedAnalytics) {
return <AdvancedAnalytics />
}
return <BasicAnalytics />
checkUsageBalance
Get the remaining balance for a usage meter tied to the customer’s subscriptions.
Parameters
usageMeterSlug: string – Slug of the usage meter to inspect.
options.subscriptionId?: string – Limit the check to a specific subscription (optional).
Returns
{ availableBalance: number } | null
Example
const usage = checkUsageBalance('api_calls')
return usage ? (
<p>{usage.availableBalance} calls remaining</p>
) : (
<p>No usage meter found.</p>
)
getProduct
Look up a specific product from the catalog that ships with billing data.
Parameters
Returns
Example
const proPlan = getProduct('pro')
return proPlan ? <h3>{proPlan.name}</h3> : <p>Product unavailable.</p>
getPrice
Look up a price by slug from the live catalog.
Parameters
Returns
Example
const monthly = getPrice('pro-monthly')
return monthly ? (
<span>
${(monthly.unitPrice / 100).toFixed(2)}
{monthly.intervalUnit ? ` / ${monthly.intervalUnit}` : ''}
</span>
) : (
<span>Price not available</span>
)
Other billing records
The billing payload also exposes higher-level billing records you can use to build richer UI:
customer – The Flowglad customer record (including email, name, IDs).
subscriptions – All subscriptions (active and past) returned for the customer.
purchases – Historical purchases for the customer.
invoices – Invoice records (including status and totals).
currentSubscription - The most recently created current subscription for the customer.
currentSubscriptions – Subscriptions currently active or pending.
paymentMethods – Saved payment methods for the customer.
billingPortalUrl – Deep link to Flowglad’s hosted billing portal (if enabled).
pricingModel – Resolved pricing model associated with the customer’s plan.
const { customer, subscriptions, paymentMethods, billingPortalUrl } = useBilling()
const activeSub = subscriptions?.find((sub) => sub.status === 'active')
const defaultMethod = paymentMethods?.[0]
Common Patterns
Feature Gating Component
import { useBilling } from '@flowglad/react'
export function FeatureGate({
featureSlug,
fallback,
children
}: {
featureSlug: string
fallback?: React.ReactNode
children: React.ReactNode
}) {
const { checkFeatureAccess } = useBilling()
if (!checkFeatureAccess) {
return null
}
if (!checkFeatureAccess(featureSlug)) {
return fallback || <div>Access denied</div>
}
return <>{children}</>
}
// Usage
<FeatureGate
featureSlug="premium_feature"
fallback={<UpgradePrompt />}
>
<PremiumFeature />
</FeatureGate>
Subscription Status Display
import { useBilling } from '@flowglad/react'
export function SubscriptionStatus() {
const { subscriptions, customer } = useBilling()
if (!subscriptions || subscriptions.length === 0) {
return <div>No active subscriptions</div>
}
const activeSubscription = subscriptions.find(s => s.status === 'active')
if (!activeSubscription) {
return <div>No active subscriptions</div>
}
return (
<div>
<h3>Current Plan</h3>
<p>Status: {activeSubscription.status}</p>
<p>Renews: {new Date(activeSubscription.currentPeriodEnd).toLocaleDateString()}</p>
</div>
)
}
import { useBilling } from '@flowglad/react'
export function UpgradeButton({ priceSlug }: { priceSlug: string }) {
const { createCheckoutSession } = useBilling()
const [isLoading, setIsLoading] = useState(false)
const handleUpgrade = async () => {
setIsLoading(true)
try {
await createCheckoutSession({
priceSlug,
successUrl: `${window.location.origin}/success`,
cancelUrl: window.location.href,
autoRedirect: true,
})
} catch (error) {
console.error('Checkout failed:', error)
setIsLoading(false)
}
}
return (
<button onClick={handleUpgrade} disabled={isLoading}>
{isLoading ? 'Loading...' : 'Upgrade'}
</button>
)
}
Server Integration
This package requires a backend server running the Flowglad Server SDK. The provider automatically makes requests to /api/flowglad by default.
Make sure you have set up the server routes:
Next Steps