Subscriptions

Managing Subscriptions

Pause, resume, cancel, and change plans. Same primitives whether you're calling from your backend, the dashboard, or the React package.

List and filter

GET /api/v1/subscriptions returns cursor-paginated results. Filter by status, interval, customerId, or externalCustomerId. Use q to search subscription ids, customer ids, plan fields, status, interval, or metadata.

Cancel

Two flavors. Pick based on your buyer experience. The backend examples use @usethrottle/subscriptions/server with your secret key.

Immediate cancellation

ts
import { createSubscriptionsClient } from '@usethrottle/subscriptions/server';

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

// Cancel immediately. Status flips to 'cancelled' now.
await subscriptions.cancel('sub_xyz', { atPeriodEnd: false });
// → POST /api/v1/subscriptions/sub_xyz/cancel  body: { atPeriodEnd: false }

Use when the buyer wants to stop right now. You may want to combine this with a prorated refund (handled outside Throttle's subscription engine).

Cancel at period end

ts
// Cancel at the end of the current period.
// Status stays 'active' until period ends, then flips to 'cancelled'.
await subscriptions.cancel('sub_xyz', { atPeriodEnd: true });
// → POST /api/v1/subscriptions/sub_xyz/cancel  body: { atPeriodEnd: true }

Common pattern: the buyer keeps access until the end of what they paid for. Throttle sets cancelAtPeriodEnd: true. The renewal cron sees it on the next tick after the period ends and finalizes the cancellation.

Either form is reversible — call cancel again with atPeriodEnd: false to undo (until the period actually ends).
To restore a subscription that was scheduled to cancel, call PATCH /api/v1/subscriptions/:id with { cancelAtPeriodEnd: false }. After actual cancellation the subscription is terminal — create a new one.

Pause and resume

ts
// Pause an active subscription. Renewal cron skips it.
await subscriptions.pause('sub_xyz');

// Resume back to active. The next periodEnd will trigger a renewal as normal.
await subscriptions.resume('sub_xyz');

Pausing an active subscription transitions it to paused. The renewal cron skips paused rows entirely — no charge attempt, no dunning. Resuming returns it to active; the period continues from where it was paused.

Guards:

  • pause() requires status active. Throws otherwise.
  • resume() requires status paused. Throws otherwise.

Change plan

Use POST /api/v1/subscriptions/:id/change-plan for all mid-cycle plan changes. The behavior depends on the effective field.

Immediate upgrade

Use effective: "now" when the buyer is moving to a more expensive plan and you want to grant access right away. Throttle charges the stored card for the full new amount and resets the billing period from today. If the card is declined the route returns 402 payment_failed and nothing changes.

ts
// Immediate upgrade: charges the stored card now, resets the billing period.
// → POST /api/v1/subscriptions/sub_xyz/change-plan
const result = await fetch('https://api.usethrottle.dev/api/v1/subscriptions/sub_xyz/change-plan', {
  method: 'POST',
  headers: { 'x-api-key': process.env.THROTTLE_SECRET_KEY!, 'content-type': 'application/json' },
  body: JSON.stringify({
    planReference: 'pro_yearly',
    planName: 'Pro Yearly',
    interval: 'yearly',
    amount: 29900,
    effective: 'now',           // <-- charge now
  }),
});
// On 402: stored card declined. Let the buyer update their payment method.

Scheduled downgrade

Use effective: "period_end" when the buyer is moving to a cheaper plan and should finish the period they paid for. No charge is made now. The four pending* fields (pendingPlanReference, pendingPlanName, pendingInterval, pendingAmount) are written, and the renewal cron applies the change on the next period end.

ts
// Scheduled downgrade: no charge now; applies on the next renewal.
// → POST /api/v1/subscriptions/sub_xyz/change-plan
const result = await fetch('https://api.usethrottle.dev/api/v1/subscriptions/sub_xyz/change-plan', {
  method: 'POST',
  headers: { 'x-api-key': process.env.THROTTLE_SECRET_KEY!, 'content-type': 'application/json' },
  body: JSON.stringify({
    planReference: 'starter_monthly',
    planName: 'Starter Monthly',
    interval: 'monthly',
    amount: 999,
    effective: 'period_end',    // <-- deferred
  }),
});
// subscription.pendingPlanReference, pendingInterval, pendingAmount are now set.
// subscription.plan_change_scheduled webhook fires.

Cancelling a pending change

If the buyer changes their mind about a scheduled downgrade, use DELETE /api/v1/subscriptions/:id/pending-change to clear the pending fields and keep the current plan. Calling this when no change is pending is a safe no-op.

ts
// Cancel a previously scheduled downgrade. The subscription stays on its current plan.
// → DELETE /api/v1/subscriptions/sub_xyz/pending-change
const result = await fetch('https://api.usethrottle.dev/api/v1/subscriptions/sub_xyz/pending-change', {
  method: 'DELETE',
  headers: { 'x-api-key': process.env.THROTTLE_SECRET_KEY! },
});
// All pending_* fields are now null. subscription.updated fires.
Precedence: cancelAtPeriodEnd wins over a pending change
If the subscription already has cancelAtPeriodEnd: true, the cancellation takes precedence and the pending plan change will never apply. Clear the cancel flag first (PATCH with { cancelAtPeriodEnd: false }), then schedule the plan change.

From the React package

tsx
// React: same operations via @usethrottle/subscriptions hooks.
import {
  useCancelSubscription,
  usePauseSubscription,
  useResumeSubscription,
  useChangePlan,
} from '@usethrottle/subscriptions';

function SubActions({ sub }) {
  const cancel = useCancelSubscription();
  const pause = usePauseSubscription();
  const resume = useResumeSubscription();
  const change = useChangePlan();

  return (
    <>
      {sub.status === 'active' && (
        <button onClick={() => pause.mutate({ id: sub.id })}>Pause</button>
      )}
      {sub.status === 'paused' && (
        <button onClick={() => resume.mutate({ id: sub.id })}>Resume</button>
      )}
      <button onClick={() => cancel.mutate({ id: sub.id, atPeriodEnd: true })}>
        Cancel at period end
      </button>
      <button onClick={() => change.mutate({ id: sub.id, planReference: 'pro_yearly', interval: 'yearly', amount: 29900 })}>
        Switch to yearly
      </button>
    </>
  );
}

The hooks invalidate the right cache entries on success — your useSubscription(id) and useSubscriptions() data update without a manual refetch.

Audit log

Every state change is recorded in the audit log with the actor (API key, user, or system) and the change set. View it in the dashboard under Customers → Subscriptions → Activity.

Next