Customer Identity
Subscriptions are scoped to customers. Throttle gives you three patterns for identifying a customer; the recommended one means you never store Throttle's IDs.
users table.Pattern A — externalCustomerId (recommended)
You give Throttle your own user ID at checkout time. Throttle upserts a customer record keyed on (workspaceId, externalId). Subsequent checkouts and API calls reuse the same record automatically.
import { createSubscriptionsClient } from '@usethrottle/subscriptions/server';
const subscriptions = createSubscriptionsClient({
apiKey: process.env.THROTTLE_SECRET_KEY!,
});
// Pass externalCustomerId everywhere. Never store Throttle's ID.
// 1. At checkout
const session = await subscriptions.createCheckoutSession({
applicationId: process.env.THROTTLE_APPLICATION_ID!,
externalCartId: `sub-${user.id}-${Date.now()}`,
returnUrl: 'https://shop.example.com/account',
cancelUrl: 'https://shop.example.com/pricing',
customer: { externalCustomerId: user.id, email: user.email },
recurring: { plan: 'pro_monthly', interval: 'monthly', amount: 2999 },
});
// 2. Listing this user's subscriptions later
const subs = await subscriptions.list({ externalCustomerId: user.id });
// 3. Looking up the canonical customer record
const customer = await subscriptions.getCustomerByExternalId(user.id);
// → GET /api/v1/customers/by-external/:externalId- No schema change in your DB.
- Idempotent — calling create twice for the same external ID does not duplicate.
- Your code reads naturally;
user.idis the identifier everywhere.
Pattern B — store Throttle's customerId
Create the Throttle customer explicitly and persist the returned cus_xyz ID in your user table. Slightly faster on hot paths because Throttle skips the upsert lookup, but you own a new column.
// Persist Throttle's customerId in your user table.
// Resolve it after checkout has created/upserted the customer.
const customer = await subscriptions.getCustomerByExternalId(user.id);
if (!customer) throw new Error('Throttle customer was not created yet');
await db.users.update({
where: { id: user.id },
data: { throttleCustomerId: customer.id },
});
// Then use Throttle's ID directly:
const subs = await subscriptions.list({ customerId: user.throttleCustomerId });- Best when you call subscription endpoints often and want to skip the resolve step.
- Adds one column to your DB (
users.throttleCustomerId). - Requires you to handle the "user exists in your DB but not yet in Throttle" case at signup time.
Pattern C — email-only
If your storefront accepts guest subscriptions (no merchant-side user account yet), you can pass customer: { email } alone. Throttle upserts on (workspaceId, email).
const session = await subscriptions.createCheckoutSession({
applicationId: process.env.THROTTLE_APPLICATION_ID!,
externalCartId: `guest-sub-${Date.now()}`,
returnUrl: 'https://shop.example.com/account',
cancelUrl: 'https://shop.example.com/pricing',
customer: { email: 'guest@example.com' },
recurring: { plan: 'pro_monthly', interval: 'monthly', amount: 2999 },
});Works for the create flow, but management is awkward — you need to look up by email to act on the subscription later, and the buyer cannot easily prove their identity from a self-service portal. Migrate guests to Pattern A as soon as they create an account.
Keeping your DB in sync via webhooks
The customer.created event fires whenever a new customer record is created — directly via API, indirectly via checkout resolution, or anywhere else. If you want to mirror Throttle's customer IDs into your DB without explicit creation calls (Pattern A → Pattern B without extra round trips), subscribe to the event.
// MERCHANT WEBHOOK — keep your DB in sync via customer.created
import { verifyThrottleWebhook } from '@/lib/throttle/webhooks';
import { createSubscriptionsClient } from '@usethrottle/subscriptions/server';
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 === 'customer.created') {
const { id, externalId, email } = event.data;
if (externalId) {
await db.users.updateMany({
where: { id: externalId },
data: { throttleCustomerId: id },
});
}
}
return Response.json({ ok: true });
}Looking up a customer by your own ID
New endpoint: GET /api/v1/customers/by-external/:externalId returns the canonical customer record (or 404). Use this when you need to fetch the Throttle row but only have your own user ID.
const customer = await subscriptions.getCustomerByExternalId('user_42');
// returns { id: 'cus_xyz', email, externalId: 'user_42', ... } or nullWhat changes in v1
GET /api/v1/subscriptionsaccepts anexternalCustomerIdquery parameter (mutually exclusive withcustomerId).GET /api/v1/customers/by-external/:externalIdis new. Pass your own ID, get the Throttle record back.- The
@usethrottle/subscriptionshooks accept eithercustomerIdorexternalCustomerIdas a scope.
Next
- Buyer Portal — how the proxy pattern uses externalCustomerId end-to-end.
- React Package — hooks accept both ID forms.