Subscriptions · Quickstart

Subscribe a Buyer in Five Minutes

End-to-end recurring billing with the Throttle embed. Three files: backend session route, frontend embed, webhook handler.

Prerequisites
You have a Throttle workspace, a secret key for your backend, a configured application, and an allowed checkout origin. If you don't, run through the payment quickstart first.

Flow

1

Create a checkout session with a recurring block

Your backend POSTs to /api/v1/checkout/sessions. The recurring block tells Throttle to vault the card and seed a subscription on payment success. Pass externalCustomerId so you can reconcile the subscription to your customer record.

2

Render the embed with the recurring prop

<PaymentEmbed sessionId={sessionId} recurring={{ plan: 'pro_monthly' }} /> handles the buyer-facing card form. Card collection, 3DS, and vault all happen inside the iframe — your storefront does not see card data.

3

Read the success payload

onSucceeded fires with { subscriptionId, customerId, paymentMethodId }. Persist customerId to your user table if you want lookups by Throttle ID, or skip it and continue using your own ID via externalCustomerId.

4

Subscribe to the webhook stream

Listen for subscription.created, subscription.renewed, subscription.past_due, and subscription.payment_failed. Your DB stays in sync without polling.

1. Backend route — create the session

The recurring block is the only difference from a one-time payment session. Throttle stores it on the session metadata and uses it to create the subscription when the embed reports a successful vault.

server
// MERCHANT BACKEND (e.g. /api/checkout/start)
import { createSubscriptionsClient } from '@usethrottle/subscriptions/server';

const subscriptions = createSubscriptionsClient({
  apiKey: process.env.THROTTLE_SECRET_KEY!,
});

export async function POST(req: Request) {
  const { user, planId } = await req.json();

  const session = await subscriptions.createCheckoutSession({
    applicationId: process.env.THROTTLE_APPLICATION_ID!,
    cartId: undefined,                            // skip cart for plan-based subs
    externalCartId: `sub-${user.id}-${Date.now()}`,
    customer: {
      externalCustomerId: user.id,                // your own user ID
      email: user.email,
      firstName: user.firstName,
      lastName: user.lastName,
    },
    returnUrl: 'https://yourstore.com/account/subscriptions',
    cancelUrl: 'https://yourstore.com/pricing',
    recurring: {
      plan: planId,                               // your own plan reference
      interval: 'monthly',
      amount: 2999,                               // cents; defaults to cart total if omitted
      trialDays: 14,                              // optional
      create: 'auto',                             // default — Throttle creates the sub
    },
  });

  return Response.json({ sessionId: session.sessionId });
}

2. Frontend — render the embed

The recurring prop on the embed is optional; when omitted, the session's recurring metadata still drives the backend. Pass it explicitly only if you want type-safe disambiguation in your client code.

client
// MERCHANT FRONTEND
'use client';
import { PaymentEmbed } from '@usethrottle/checkout-react';
import { useRouter } from 'next/navigation';

export function SubscribeForm({ sessionId }: { sessionId: string }) {
  const router = useRouter();

  return (
    <PaymentEmbed
      sessionId={sessionId}
      parentOrigin="https://yourstore.com"
      baseUrl="https://checkout.usethrottle.dev"
      onSucceeded={(result) => {
        // result.subscriptionId is populated when recurring + create:'auto'
        router.push(`/account/subscriptions/${result.subscriptionId}`);
      }}
      onFailed={(err) => console.error('Subscribe failed:', err)}
    />
  );
}

3. Webhook handler — react to lifecycle events

Webhooks are the source of truth for state. The embed callback is convenient but not durable — a buyer who closes their tab still triggers webhooks.

server
// MERCHANT BACKEND (/api/webhooks/throttle)
import { verifyThrottleWebhook } from '@/lib/throttle/webhooks';

export async function POST(req: Request) {
  const rawBody = await req.text();
  const signature = req.headers.get('x-throttle-signature');
  if (!signature || !verifyThrottleWebhook(rawBody, signature, process.env.THROTTLE_WEBHOOK_SECRET!)) {
    return new Response('Invalid signature', { status: 400 });
  }

  const event = JSON.parse(rawBody);

  switch (event.type) {
    case 'subscription.created':
      // Provision Pro features for event.data.subscription.customerId
      break;
    case 'subscription.payment_failed':
      // Send a "your card was declined" email
      break;
    case 'subscription.cancelled':
      // De-provision; check event.data.reason for 'dunning_exhausted'
      break;
    case 'subscription.renewed':
      // Optional: ledger entry for the new billing period
      break;
  }

  return Response.json({ ok: true });
}
Want to customize who creates the subscription?
Set recurring.create: 'manual' if you'd rather call subscriptions.create from your own backend after vault success. See Creating Subscriptions for the three patterns.

Next