Subscriptions

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.

The default recommendation
Use Pattern A — externalCustomerId. Pass your own user ID everywhere. Throttle resolves it on every call. No new column on your 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.

ts
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.id is 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.

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

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

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

ts
const customer = await subscriptions.getCustomerByExternalId('user_42');
// returns { id: 'cus_xyz', email, externalId: 'user_42', ... } or null

What changes in v1

  • GET /api/v1/subscriptions accepts an externalCustomerId query parameter (mutually exclusive with customerId).
  • GET /api/v1/customers/by-external/:externalId is new. Pass your own ID, get the Throttle record back.
  • The @usethrottle/subscriptions hooks accept either customerId or externalCustomerId as a scope.

Next