Base 3:16
Guides

Move subscribers to a new plan version

Publish a better version of a plan and give it to the customers already on it — in place, without restarting their billing period — or keep them where they are.

A subscription points at the plan version it was sold on, and versions are immutable, so publishing new pricing never reprices anybody. The other side of that rule: publishing a better version — a new feature, a higher allowance, the same price — reaches nobody already subscribed until you move them.

That move is a migration.

Lateral, or not

Base316 compares the version a subscriber is on with the one you are moving them to:

ClassificationMeans
LATERALEvery price identical, and nothing taken away: no feature lost, no limit lowered.
PRICE_CHANGEAny price differs, up or down.
REDUCTIONA feature lost, a limit lowered, or a hard cap, reset interval or rollover changed against the customer.

A version that reprices and takes something away is a REDUCTION.

Policies

PolicyWhat happens to subscribers of older versions
GRANDFATHERThey stay where they are. The default.
MIGRATE_NOWMoved now, in place: same subscription id, same period, nothing billed. Lateral only.
MIGRATE_AT_PERIOD_ENDMoved at each subscription's next period boundary. Anything not lateral needs allowNonLateral: true.
AUTOMIGRATE_NOW when the change is lateral, otherwise GRANDFATHER.

A lateral move does not restart the billing period, issue an invoice or credit anything — the customer is simply entitled to more from that moment. Usage already recorded this period is rated on the new version, which charges the same or less by definition.

A trialing subscription moves at once under either timing: nothing is charged during a trial, so moving now and at conversion bill the same.

Skipped, with the reason recorded: a subscription with a pending schedule (a plan change, a ramp or an earlier migration), one cancelling at period end, one in another currency, and one whose custom prices cannot be matched to the new version exactly.

On publish: what a catalog sync wants

Pass propagation when you create or publish a version. AUTO is the setting for a script that keeps your catalog in step with code: every same-price improvement reaches existing customers, and every price change leaves them alone.

await client.catalog.plans.createVersion(planId, {
	currency: "USD",
	interval: "MONTH",
	prices: [{ model: "FLAT", amount: "99.00" }],
	entitlements,
	propagation: "AUTO",
});

The answer's propagation says what happened (migrated, skipped, and the migration id). Set it once for a plan instead with defaultPropagation:

await client.catalog.plans.update(planId, { defaultPropagation: "AUTO" });

Only AUTO and MIGRATE_NOW act on publish. A period-end move can change what somebody pays, so it always goes through a preview.

Preview, then migrate

Every migration is confirmed against a preview made in the last fifteen minutes, so what moves is exactly what you saw.

const preview = await client.catalog.plans.previewMigration(planId, versionId, {
	policy: "MIGRATE_AT_PERIOD_END",
	allowNonLateral: true,
});

// preview.versions — subscribers per old version, and how many move, wait or skip
// preview.items    — per subscription: entitlement changes, feeDelta, outcome, reason

const run = await client.catalog.plans.migrateSubscribers(planId, versionId, {
	policy: "MIGRATE_AT_PERIOD_END",
	allowNonLateral: true,
	previewToken: preview.previewToken,
});

If a subscriber joined or changed since the preview, the migration is refused with CONFLICT — preview again. Up to 500 subscriptions move before the call returns; a larger migration answers PENDING and finishes in the background. Follow it with client.catalog.migrations.get(run.migrationId).

Finding subscribers left behind

GET /v1/subscriptions/outdated lists every subscription not on its plan's latest published version, with how its gap classifies. LATERAL rows are customers being refused features they already pay for. Pass subscriptionIds to the preview and the migration to move exactly those.

const { data } = await client.subscriptions.listOutdated({ planId });
const lateral = data.filter((row) => row.classification === "LATERAL");

Webhooks

  • subscription.plan_version_changed — a subscription moved onto a newer version, now or at its period end: previousPlanVersionId, planVersionId, timing, classification, migrationId and the entitlement changes. The subscription id is unchanged, unlike subscription.updated with change: "plan_changed", which is a customer changing plans.
  • entitlement.updated — what a customer is entitled to changed. Carries the customer, not the entitlements: fetch them, or refresh any token carrying the entitlements scope (those claims last at most five minutes).
  • A period-end move is announced when scheduled as subscription.updated with change: "plan_version_change_scheduled".

Every move is also in the audit log, and every migration keeps a record of what happened to each subscription in it.

On this page