Skip to content

Migrate subscribers to a new price

The subscriptions.migrations.create operation: POST /api/v1/subscriptions/migrations in the REST API and the subscriptions_migrations_create tool in the MCP server.

POST/api/v1/subscriptions/migrations

Moves the subscriptions on one price to another at their renewals, after a notice (spec §16.14). Prices can't change once used, so changing what existing subscribers pay is a migration to a new price that bills alike (the same currency, interval and usage type, and meter for metered prices).

  1. Every matching subscription (an item on fromPriceId; filter narrows by status, default trialing, active and past due, and by customer) gets a scheduled change (pendingUpdate) that swaps the item's price at its first renewal on or after the earliest change: noticeDays from now (effective: next_renewal, default) or effectiveDate (effective: date, itself at least noticeDays away). Quantities stay.
  2. Each customer is emailed a notice noticeDays (default 30) before their change.
  3. The renewal applies the change and bills the new price.
  4. subscriptions.migrations.get reports progress: scheduled, notified, applied, pending and skipped.

Subscriptions with another change already scheduled, or set to cancel, are skipped (skippedAtCreation). At most 1,000 subscriptions per migration. Run it as a dry run first: it returns the counts and the monthly recurring revenue before and after without changing anything. Each scheduled subscription emits subscription.updated.

At a glance

  • REST: POST /api/v1/subscriptions/migrations
  • MCP tool: subscriptions_migrations_create
  • Operation ID: subscriptions.migrations.create
  • Scope: Brand
  • Permission: subscriptions:write
  • Risk tier: financial
  • Dry run: Supported (send Ownrecord-Dry-Run: true)
  • Idempotency: Idempotency-Key required
  • Modes: Test and live
  • Authentication: API key with the subscriptions:write scope
  • Since: M5

Request

Body

A JSON object (Content-Type: application/json):

FieldTypeRequiredDescription
fromPriceIdprice_… IDYesThe price subscribers pay today (price_…).
toPriceIdprice_… IDYesThe price they move to: an active recurring price with the same currency, interval and usage type.
effectiveone of next_renewal, dateNonext_renewal: each subscription changes at its first renewal at least noticeDays away; date: at its first renewal on or after effectiveDate.
effectiveDatedate-timeNoWith effective: date: the earliest change (at least noticeDays from now).
noticeDaysinteger (0 to 365)NoDays of notice: the customers are emailed this many days before their change (default 30; at least what your terms promise).
filterobjectNo
filter.statusesarray of one of trialing, active, past_due, unpaid, pausedNoOnly subscriptions in these statuses (default: trialing, active, past_due).
filter.customerIdsarray of cus_… IDNoOnly these customers' subscriptions.

Response

201 Created with a JSON object:

FieldTypeDescription
idsmig_… ID
object"subscription_migration"
livemodeboolean
brandIdbrd_… ID
fromPriceIdprice_… ID
toPriceIdprice_… ID
currencystring
effectiveone of next_renewal, date
effectiveDatedate-time or null
noticeDaysinteger
filterobject
filter.statusesarray of one of incomplete, incomplete_expired, trialing, active, past_due, unpaid, paused, canceled
filter.customerIdsarray of cus_… ID or null
statusone of scheduled, completedscheduled while changes are still waiting for their renewal; completed once none is.
countsobjectProgress (spec §16.14).
counts.matchedinteger (at least 0)Subscriptions on the old price that the filter matched.
counts.scheduledinteger (at least 0)Of those, the ones given the change.
counts.notifiedinteger (at least 0)Customers sent their notice.
counts.appliedinteger (at least 0)Changes applied at a renewal.
counts.pendinginteger (at least 0)Changes still waiting for their renewal.
counts.skippedinteger (at least 0)Matched but not changed: skipped at once (skippedAtCreation), or dropped before their renewal because the subscription was canceled or changed again.
skippedAtCreationobjectWhy matching subscriptions weren't changed: pending_update (another change was already waiting), cancel_scheduled (set to end at its period end).
revenueobjectThe monthly recurring amount of the changed items before and after (licensed items; metered items are billed from usage).
revenue.mrrBeforemoney
revenue.mrrAftermoney
revenue.mrrDeltamoney
firstChangeAtdate-time or nullThe first scheduled change.
lastChangeAtdate-time or nullThe last scheduled change.
versioninteger
createdAtdate-time
updatedAtdate-time

Examples

Move monthly subscribers to the new price at their next renewal after 30 days

HTTP
POST /api/v1/subscriptions/migrations
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json

{
  "fromPriceId": "price_01jacq4m3xf0v8w9zq2y7k5n6p",
  "toPriceId": "price_01jacq4m3xf0v8w9zq2y7k5n6q"
}

Response (201):

JSON
{
  "id": "smig_01jacq4m3xf0v8w9zq2y7k5n6h",
  "object": "subscription_migration",
  "livemode": false,
  "brandId": "brd_01jacq4m3xf0v8w9zq2y7k5n6b",
  "fromPriceId": "price_01jacq4m3xf0v8w9zq2y7k5n6p",
  "toPriceId": "price_01jacq4m3xf0v8w9zq2y7k5n6q",
  "currency": "EUR",
  "effective": "next_renewal",
  "effectiveDate": null,
  "noticeDays": 30,
  "filter": {
    "statuses": ["trialing", "active", "past_due"],
    "customerIds": null
  },
  "status": "scheduled",
  "counts": {
    "matched": 42,
    "scheduled": 40,
    "notified": 0,
    "applied": 0,
    "pending": 40,
    "skipped": 2
  },
  "skippedAtCreation": {
    "pending_update": 2,
    "cancel_scheduled": 0
  },
  "revenue": {
    "mrrBefore": {
      "amount": "400000000",
      "currency": "EUR"
    },
    "mrrAfter": {
      "amount": "480000000",
      "currency": "EUR"
    },
    "mrrDelta": {
      "amount": "80000000",
      "currency": "EUR"
    }
  },
  "firstChangeAt": "2026-11-20T10:00:00.000Z",
  "lastChangeAt": "2026-12-19T10:00:00.000Z",
  "version": 2,
  "createdAt": "2026-10-20T09:00:00.000Z",
  "updatedAt": "2026-10-20T09:00:00.000Z"
}

Errors

CodeHTTP statusWhenWhat to do
validation_failed400A field is malformed, or effectiveDate is missing, misplaced or less than noticeDays away.Fix the fields listed in details.issues and send the request again.
resource_not_found404No price with this ID exists for the brand in this mode.Check the ID and that your credentials are scoped to the right organization, entity, brand and mode.
subscription_migration_invalid400The prices don't bill alike (currency, interval, usage type, meter), are the same, aren't recurring, or the new one is archived (details.reason).Migrate between two recurring prices that bill alike: the same currency, interval and usage type (and meter, for metered prices). The new price must be active.
subscription_migration_too_large400More than 1,000 subscriptions match: narrow the filter and run several migrations.Narrow it with filter.customerIds or filter.statuses, and run several migrations. One migration schedules at most 1,000 subscriptions.
version_conflict409A matching subscription changed at the same time; retry.Fetch the object again and retry the change.
permission_denied403The credentials can't change this brand's subscriptions (subscriptions:write).Ask an organization owner or admin for a role or scope that allows this action.

It can also return these common errors: api_version_unsupported, invalid_json, unsupported_media_type, authentication_required, api_key_invalid, scope_required, idempotency_key_required, idempotency_key_reused, idempotency_in_progress, approval_required, approval_rejected, approval_expired, stale_approval, approval_policy_denied, rate_limited, internal_error.