Creating Subscriptions
Three patterns map to three integration trade-offs. Pick the one that fits how much control you want over the create call.
plan, planName, amount, and interval values you pass on the recurring block come from your application — not from Throttle. We treat planReference as opaque text scoped to your merchant and never validate it against any list. Two workspaces can both have a'pro_monthly' reference without collision.Most teams keep plans in their own database (a
plans table or a static config in the repo). That source-of-truth is what your storefront pulls from to render the plan picker, and what your backend forwards into the recurring block. A managed plan catalog inside Throttle is not part of v1.Pattern A — Server auto-creates (default)
Set recurring.create: 'auto' (the default) and Throttle creates the subscription atomically when the card is vaulted. Your onSucceeded callback fires with subscriptionId already populated.
import { createSubscriptionsClient } from '@usethrottle/subscriptions/server';
const subscriptions = createSubscriptionsClient({
apiKey: process.env.THROTTLE_SECRET_KEY!,
});
// recurring.create defaults to 'auto'.
// Throttle creates the subscription as soon as the card is vaulted.
const session = await subscriptions.createCheckoutSession({
applicationId, externalCartId, returnUrl, cancelUrl,
customer: { externalCustomerId: user.id, email: user.email },
recurring: { plan: 'pro_monthly', interval: 'monthly', amount: 2999 },
});
// In the embed:
<PaymentEmbed
sessionId={session.sessionId}
parentOrigin="https://shop.example.com"
onSucceeded={({ subscriptionId }) => router.push(`/account/${subscriptionId}`)}
/>;Use this when you want the path of least resistance. The merchant writes zero code on the success path — vault, sub create, and notification all happen on Throttle's side.
subscription.create_failed with a reason code. Listen for it and decide your fallback: retry from your own backend, contact support, or surface the error to the buyer.Pattern B — Merchant backend creates explicitly
Set recurring.create: 'manual' when you want full control. The embed only vaults; you call subscriptions.create yourself.
// Set create:'manual' to opt out of auto-create.
const session = await subscriptions.createCheckoutSession({
// ...
recurring: { plan: 'pro_monthly', interval: 'monthly', amount: 2999, create: 'manual' },
});
// onSucceeded is a UI signal. Your backend creates the subscription
// after the checkout has vaulted the card for this externalCustomerId.
<PaymentEmbed
sessionId={session.sessionId}
parentOrigin="https://shop.example.com"
onSucceeded={async ({ orderId, paymentId }) => {
await fetch('/api/subscribe', {
method: 'POST',
body: JSON.stringify({ orderId, paymentId, plan: 'pro_monthly' }),
});
}}
/>;// MERCHANT BACKEND — call subscriptions.create yourself
import { createSubscriptionsClient } from '@usethrottle/subscriptions/server';
const subscriptions = createSubscriptionsClient({
apiKey: process.env.THROTTLE_SECRET_KEY!,
});
export async function POST(req: Request) {
const { userId, plan } = await req.json();
const now = new Date();
const periodEnd = new Date(now);
periodEnd.setMonth(periodEnd.getMonth() + 1);
const sub = await subscriptions.create({
externalCustomerId: userId,
planReference: plan,
interval: 'monthly',
amount: 2999,
currentPeriodStart: now.toISOString(),
currentPeriodEnd: periodEnd.toISOString(),
metadata: { source: 'manual_post_checkout' },
});
// Persist your own mapping if needed
await db.userSubscriptions.create({ userId, subId: sub.id });
return Response.json({ subscriptionId: sub.id });
}Use this when you need to look up plan details from your own DB, run business logic before subscribing, or attach merchant-specific metadata that the embed cannot know about.
Pattern C — Webhook-driven creation
Use the payment.vaulted webhook. Survives a buyer closing their tab between vault and create. Recommended when reliability matters more than latency.
// MERCHANT BACKEND — webhook-driven creation.
// Trigger fires regardless of create mode; useful for survival across tab-close.
import { createSubscriptionsClient } from '@usethrottle/subscriptions/server';
import { verifyThrottleWebhook } from '@/lib/throttle/webhooks';
const subscriptions = createSubscriptionsClient({
apiKey: process.env.THROTTLE_SECRET_KEY!,
});
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);
if (event.type !== 'payment.vaulted') return Response.json({ ok: true });
const { customerId, paymentMethodId, recurring } = event.data;
if (!recurring) return Response.json({ ok: true }); // not a subscription intent
// Idempotent: only create if we haven't already (auto mode may have beaten us)
const existing = await subscriptions.list({ customerId, status: 'active' });
if (existing.data.some((s) => s.metadata?.checkoutSessionId === event.data.sessionId)) {
return Response.json({ ok: true });
}
await subscriptions.create({
customerId,
planReference: recurring.plan,
interval: recurring.interval,
amount: recurring.amount,
currentPeriodStart: new Date().toISOString(),
currentPeriodEnd: addInterval(new Date(), recurring.interval).toISOString(),
metadata: { checkoutSessionId: event.data.sessionId },
});
return Response.json({ ok: true });
}Combine with create: 'manual' on the embed: auto-mode and webhook-driven creation both racing to call create would double-bill (Throttle de-dupes, but it's still cleaner to pick one).
Choosing
| Goal | Use |
|---|---|
| Fastest to integrate | Pattern A (auto) |
| Custom plan/business logic | Pattern B (manual) |
| Survives tab-close, weak network | Pattern C (webhook) |
| Server-side only (no embed) | Direct subscriptions.create with a pre-vaulted payment method ID |
Direct creation (no embed)
For B2B flows where you've already collected a card via another mechanism (a sales rep enters it through your admin tool), call subscriptions.create directly. Pre-condition: a customerPaymentMethods row already exists with isDefault: true for the customer — otherwise the renewal cron has nothing to charge.
Next
- Customer Identity — how to identify a customer without storing Throttle's ID.
- Subscription Webhooks — full event reference.