Subscriptions

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.

Use the backend proxy
A proxy keeps your 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

  1. Buyer's browser calls /api/throttle/api/v1/subscriptions on your domain.
  2. Your backend authenticates the user, verifies they own the resource being mutated, and forwards the call to Throttle with the secret key.
  3. @usethrottle/subscriptions's SubscriptionProvider uses a custom fetcher that 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
// 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.

ts
// 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).

ts
// 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;
}
Critical
Without ownership checks on mutations, any logged-in user could cancel any subscription by guessing IDs. Always verify ownership server-side before forwarding a mutating request.

Wiring the React provider

tsx
// 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 active subscription and resume later. Pause is not offered on a trialing subscription (the API refuses it with 400 invalid_subscription_state); @usethrottle/auth 0.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-method for the buyer's own subscriptions (setPaymentMethod() on the server client). See Change the card .
  • Surface a past_due subscription with an update-card action. The <DunningBanner /> primitive gives you the UI hook; your backend owns the actual card-refresh flow.
Installment plans: progress only, no Cancel/Pause/Resume
An installment plan (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