React Package · @usethrottle/subscriptions
Production React hooks, server proxy helpers, and primitive components for managing Throttle subscriptions end-to-end from your buyer portal.
createSubscriptionsClient from the server entrypoint and use it with your secret key. The package never requires putting an sk_* key in browser code.Install
npm install @usethrottle/subscriptions @usethrottle/checkout-reactBackend proxy
The package ships a Next.js-compatible proxy helper at @usethrottle/subscriptions/server. It allowlists the subscription, customer-by-external, trial eligibility, and checkout-session paths, pins subscription reads, customer lookups, direct subscription creates, and checkout-session creates to the authenticated buyer's externalCustomerId, and verifies ownership before forwarding subscription mutations.
// app/api/throttle/[...path]/route.ts
import { createSubscriptionProxyHandler } from '@usethrottle/subscriptions/server';
import { auth } from '@/lib/auth';
const handler = createSubscriptionProxyHandler({
apiKey: process.env.THROTTLE_SECRET_KEY!,
async getExternalCustomerId() {
const user = await auth();
return user?.id ?? null;
},
});
export { handler as GET, handler as POST, handler as PATCH };Provider
Mount a SubscriptionProvider high in your tree. The fetcher prop is the contract with your backend proxy — every hook routes through it.
'use client';
import { SubscriptionProvider } from '@usethrottle/subscriptions';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<SubscriptionProvider
fetcher={async (path, init) => fetch(`/api/throttle${path}`, init)}
>
{children}
</SubscriptionProvider>
);
}Server-side client
Backend routes can use the server entrypoint as a typed subscription client without bundling React provider code. Use this for admin tooling, webhook handlers, or custom business logic that should run server-side.
import { createSubscriptionsClient } from '@usethrottle/subscriptions/server';
const subscriptions = createSubscriptionsClient({
apiKey: process.env.THROTTLE_SECRET_KEY!,
});
await subscriptions.cancel('sub_123', { atPeriodEnd: true });Subscription confirmation pages and account portals can also fetch vaulted payment-method summaries without reaching around the package.
import { createSubscriptionsClient } from '@usethrottle/subscriptions/server';
const subscriptions = createSubscriptionsClient({
apiKey: process.env.THROTTLE_SECRET_KEY!,
});
const paymentMethods = await subscriptions.listCustomerPaymentMethods('cus_123');Hooks · queries
useSubscriptions(filters)
Lists subscriptions for a customer.
import { useSubscriptions } from '@usethrottle/subscriptions';
function MySubs() {
const { data, isLoading, error } = useSubscriptions({
externalCustomerId: 'user_42', // or { customerId: 'cus_xyz' }
status: 'active', // optional
});
if (isLoading) return <Spinner />;
if (error) return <ErrorView error={error} />;
return data.data.map((sub) => <SubRow key={sub.id} subscription={sub} />);
}Filters: customerId or externalCustomerId (one is required), plus optional status, cursor, limit.
useSubscription(id)
Fetches a single subscription.
import { useSubscription } from '@usethrottle/subscriptions';
function SubDetail({ id }: { id: string }) {
const { data: sub, isLoading } = useSubscription(id);
if (isLoading || !sub) return <Spinner />;
return <SubCard subscription={sub} />;
}useCustomerPaymentMethods(customerId)
Fetches saved payment-method summaries. When routed through the backend proxy, payment method reads are pinned to the authenticated buyer's customer row.
import { useCustomerPaymentMethods } from '@usethrottle/subscriptions';
function SavedCards({ customerId }: { customerId: string }) {
const { data: methods, isLoading } = useCustomerPaymentMethods(customerId);
if (isLoading) return <Spinner />;
return methods?.map((method) => (
<span key={method.id}>
{method.cardBrand} ending in {method.cardLastFour}
</span>
));
}Hooks · mutations
All mutation hooks return a { mutate, mutateAsync, isPending, error, data } shape. On success they invalidate the relevant useSubscription / useSubscriptions caches.
useCancelSubscription()
import { useCancelSubscription } from '@usethrottle/subscriptions';
function CancelButton({ subId }: { subId: string }) {
const cancel = useCancelSubscription();
return (
<button
disabled={cancel.isPending}
onClick={() => cancel.mutate({ id: subId, atPeriodEnd: true })}
>
{cancel.isPending ? 'Cancelling…' : 'Cancel at period end'}
</button>
);
}usePauseSubscription() / useResumeSubscription()
import { usePauseSubscription, useResumeSubscription } from '@usethrottle/subscriptions';
function PauseToggle({ sub }) {
const pause = usePauseSubscription();
const resume = useResumeSubscription();
return sub.status === 'paused'
? <button onClick={() => resume.mutate({ id: sub.id })}>Resume</button>
: <button onClick={() => pause.mutate({ id: sub.id })}>Pause</button>;
}useChangePlan()
import { useChangePlan } from '@usethrottle/subscriptions';
function PlanSwitcher({ subId }: { subId: string }) {
const change = useChangePlan();
return (
<button onClick={() => change.mutate({
id: subId,
planReference: 'pro_yearly',
interval: 'yearly',
amount: 29900,
})}>
Switch to yearly
</button>
);
}useCreateCheckoutSession()
Bridge from the management package to @usethrottle/checkout-react's <PaymentEmbed />. Wraps the /api/v1/checkout/sessions POST with recurring metadata.
import { useCreateCheckoutSession } from '@usethrottle/subscriptions';
import { PaymentEmbed } from '@usethrottle/checkout-react';
function SubscribeFlow({ user, plan }) {
const create = useCreateCheckoutSession();
if (!create.data) {
return (
<button onClick={() => create.mutate({
applicationId: '00000000-0000-0000-0000-000000000000',
externalCartId: `sub-${user.id}-${plan}`,
customer: { externalCustomerId: user.id, email: user.email },
returnUrl: 'https://shop.example.com/account',
cancelUrl: 'https://shop.example.com/pricing',
recurring: { plan, interval: 'monthly', amount: 2999 },
})}>
Subscribe
</button>
);
}
return (
<PaymentEmbed
sessionId={create.data.sessionId}
parentOrigin="https://shop.example.com"
onSucceeded={({ subscriptionId }) => router.push(`/account/${subscriptionId}`)}
/>
);
}useTrialEligibility()
Calls POST /api/v1/subscriptions/eligibility-check through your proxy. Use it when your UI needs to decide whether to offer a trial after a payment method has already been vaulted.
import { useTrialEligibility } from '@usethrottle/subscriptions';
function TrialGate({ paymentMethodId }: { paymentMethodId: string }) {
const check = useTrialEligibility();
return (
<button onClick={() => check.mutate({ paymentMethodId })}>
Check trial eligibility
</button>
);
}Primitive components
Three presentational components. They take a subscription (or a single field) as a prop and render appropriately. No secret-key API calls.
import {
SubscriptionStatusBadge,
TrialCountdown,
DunningBanner,
} from '@usethrottle/subscriptions';
function SubCard({ subscription: sub }) {
return (
<article>
<h3>{sub.planName}</h3>
<SubscriptionStatusBadge status={sub.status} />
{sub.status === 'trialing' && sub.trialEnd && (
<TrialCountdown endDate={sub.trialEnd} />
)}
{sub.status === 'past_due' && (
<DunningBanner subscription={sub} onUpdateCard={() => openUpdateCardFlow(sub)} />
)}
<p>Next charge: {sub.currentPeriodEnd}</p>
</article>
);
}<SubscriptionStatusBadge status={status} />— chip with state-specific colour. AcceptsclassName.<TrialCountdown endDate={date} />— "Trial ends in N days". Pass the trial end date.<DunningBanner subscription={sub} onUpdateCard={fn} />— shown when status ispast_due. Includes an action that calls your update-card flow.
Boundaries
- Payment collection stays in
@usethrottle/checkout-react. This package creates recurring sessions and manages the subscription lifecycle around the embed. - Direct browser auth via customer-scoped tokens is not exposed yet. Use the proxy helper so your backend owns authorization and secret-key storage.
- Composite portal layouts are application code. Components here are primitives and accept
className/stylefor your design system.