Buyer Portal
Let buyers manage their own subscriptions. v1 uses a proxy pattern: your backend forwards requests from the browser to Throttle, holding the secret key on your side and enforcing ownership.
sk_* key off the wire and lets you
reuse your existing user authentication. The hook API in @usethrottle/subscriptions is designed around this
production path.
Architecture
-
Buyer's browser calls
/api/throttle/api/v1/subscriptionson your domain. - Your backend authenticates the user, verifies they own the resource being mutated, and forwards the call to Throttle with the secret key.
-
@usethrottle/subscriptions'sSubscriptionProvideruses a customfetcherthat targets the proxy.
Next.js App Router template
A single catch-all route handler proxies the relevant subscription endpoints. The helper
pins externalCustomerId server-side on every read,
rewrites checkout-session creates to the authenticated buyer, resolves payment-method
reads from the buyer's customer row, and verifies ownership before subscription
mutations.
// app/api/throttle/[...path]/route.ts
// Next.js App Router proxy. Forwards authenticated requests to Throttle.
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 }; Express template
Per-route variant. Slightly more code, but easier to reason about route-by-route authorization.
// MERCHANT BACKEND — Express variant.
import { createSubscriptionsClient } from '@usethrottle/subscriptions/server';
const subscriptions = createSubscriptionsClient({
apiKey: process.env.THROTTLE_SECRET_KEY!,
});
app.get('/api/throttle/api/v1/subscriptions', requireAuth, async (req, res) => {
const result = await subscriptions.list({
externalCustomerId: req.user.id,
status: req.query.status,
});
res.json(result);
});
app.post('/api/throttle/api/v1/subscriptions/:id/cancel', requireAuth, async (req, res) => {
if (!(await assertUserOwnsSub(req.user.id, req.params.id))) return res.sendStatus(403);
const sub = await subscriptions.cancel(req.params.id, {
atPeriodEnd: req.body?.atPeriodEnd ?? true,
});
res.json(sub);
});
// Repeat for: /pause, /resume, PATCH plan changes, GET single sub. Authorization
Throttle's API key authorizes your account to act on any subscription you own. It
does not know which buyer owns which subscription. That mapping lives in your DB (or you
derive it via customer.externalId === user.id).
// Verify the buyer owns the subscription before mutating.
async function assertUserOwnsSub(userId: string, subId: string) {
const sub = await subscriptions.get(subId);
if (!sub) return false;
const customer = await subscriptions.getCustomerByExternalId(userId);
return customer?.id === sub.customerId;
} Wiring the React provider
// Mount the React provider with a fetcher that hits your proxy.
'use client';
import { SubscriptionProvider } from '@usethrottle/subscriptions';
export function ThrottleProvider({ children }: { children: React.ReactNode }) {
return (
<SubscriptionProvider
fetcher={async (path, init) => {
return fetch(`/api/throttle${path}`, init);
}}
>
{children}
</SubscriptionProvider>
);
}
Every hook in @usethrottle/subscriptions calls
through this fetcher. Your proxy is the only thing that talks to Throttle.
What buyers can do
- List their subscriptions and see status, plan, next billing date.
-
Pause an
activesubscription and resume later. Pause is not offered on atrialingsubscription (the API refuses it with400 invalid_subscription_state);@usethrottle/auth0.6.1 and later hide the button there. - Cancel at period end (or immediately, if you allow it).
- Change plan (your UI decides which plans are offered).
-
Move one subscription to another of the buyer's saved cards: the proxy forwards
PATCH /api/v1/subscriptions/:id/payment-methodfor the buyer's own subscriptions (setPaymentMethod()on the server client). See Change the card . -
Surface a
past_duesubscription with an update-card action. The<DunningBanner />primitive gives you the UI hook; your backend owns the actual card-refresh flow.
totalPayments set) shows its cadence
and payment progress — installmentProgress() from @usethrottle/subscriptions gives you paymentsMade, totalPayments,
and the remaining amount — but your proxy must hide Cancel, Pause, and Resume for it and
refuse to forward those calls. Throttle's subscription API does not itself refuse a
pause/resume/cancel called with your merchant secret key. The @usethrottle/subscriptions proxy now refuses every buyer
mutation of an installment plan by default (403
installment_plan_merchant_only), except changing its card (
PATCH …/payment-method), which is not one of the plan's
locked terms; if you pass your own authorizeMutation, the default is replaced and your function
must check totalPayments != null itself. (Throttle's
separate customer-session storefront endpoints, documented under Storefront Auth,
enforce this server-side and return 403 installment_plan_merchant_only for a buyer session.)
Next
- React Package — full hook reference.