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

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`):

| Field                | Type                                                                 | Required | Description                                                                                                                                          |
| -------------------- | -------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fromPriceId`        | `price_…` ID                                                         | Yes      | The price subscribers pay today (`price_…`).                                                                                                         |
| `toPriceId`          | `price_…` ID                                                         | Yes      | The price they move to: an active recurring price with the same currency, interval and usage type.                                                   |
| `effective`          | one of `next_renewal`, `date`                                        | No       | `next_renewal`: each subscription changes at its first renewal at least `noticeDays` away; `date`: at its first renewal on or after `effectiveDate`. |
| `effectiveDate`      | date-time                                                            | No       | With `effective: date`: the earliest change (at least `noticeDays` from now).                                                                        |
| `noticeDays`         | integer (0 to 365)                                                   | No       | Days of notice: the customers are emailed this many days before their change (default 30; at least what your terms promise).                         |
| `filter`             | object                                                               | No       |                                                                                                                                                      |
| `filter.statuses`    | array of one of `trialing`, `active`, `past_due`, `unpaid`, `paused` | No       | Only subscriptions in these statuses (default: trialing, active, past_due).                                                                          |
| `filter.customerIds` | array of `cus_…` ID                                                  | No       | Only these customers' subscriptions.                                                                                                                 |

## Response

`201 Created` with a JSON object:

| Field                | Type                                                                                                                 | Description                                                                                                                                             |
| -------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                 | `smig_…` ID                                                                                                          |                                                                                                                                                         |
| `object`             | `"subscription_migration"`                                                                                           |                                                                                                                                                         |
| `livemode`           | boolean                                                                                                              |                                                                                                                                                         |
| `brandId`            | `brd_…` ID                                                                                                           |                                                                                                                                                         |
| `fromPriceId`        | `price_…` ID                                                                                                         |                                                                                                                                                         |
| `toPriceId`          | `price_…` ID                                                                                                         |                                                                                                                                                         |
| `currency`           | string                                                                                                               |                                                                                                                                                         |
| `effective`          | one of `next_renewal`, `date`                                                                                        |                                                                                                                                                         |
| `effectiveDate`      | date-time or null                                                                                                    |                                                                                                                                                         |
| `noticeDays`         | integer                                                                                                              |                                                                                                                                                         |
| `filter`             | object                                                                                                               |                                                                                                                                                         |
| `filter.statuses`    | array of one of `incomplete`, `incomplete_expired`, `trialing`, `active`, `past_due`, `unpaid`, `paused`, `canceled` |                                                                                                                                                         |
| `filter.customerIds` | array of `cus_…` ID or null                                                                                          |                                                                                                                                                         |
| `status`             | one of `scheduled`, `completed`                                                                                      | `scheduled` while changes are still waiting for their renewal; `completed` once none is.                                                                |
| `counts`             | object                                                                                                               | Progress (spec §16.14).                                                                                                                                 |
| `counts.matched`     | integer (at least 0)                                                                                                 | Subscriptions on the old price that the filter matched.                                                                                                 |
| `counts.scheduled`   | integer (at least 0)                                                                                                 | Of those, the ones given the change.                                                                                                                    |
| `counts.notified`    | integer (at least 0)                                                                                                 | Customers sent their notice.                                                                                                                            |
| `counts.applied`     | integer (at least 0)                                                                                                 | Changes applied at a renewal.                                                                                                                           |
| `counts.pending`     | integer (at least 0)                                                                                                 | Changes still waiting for their renewal.                                                                                                                |
| `counts.skipped`     | integer (at least 0)                                                                                                 | Matched but not changed: skipped at once (`skippedAtCreation`), or dropped before their renewal because the subscription was canceled or changed again. |
| `skippedAtCreation`  | object                                                                                                               | Why matching subscriptions weren't changed: `pending_update` (another change was already waiting), `cancel_scheduled` (set to end at its period end).   |
| `revenue`            | object                                                                                                               | The monthly recurring amount of the changed items before and after (licensed items; metered items are billed from usage).                               |
| `revenue.mrrBefore`  | money                                                                                                                |                                                                                                                                                         |
| `revenue.mrrAfter`   | money                                                                                                                |                                                                                                                                                         |
| `revenue.mrrDelta`   | money                                                                                                                |                                                                                                                                                         |
| `firstChangeAt`      | date-time or null                                                                                                    | The first scheduled change.                                                                                                                             |
| `lastChangeAt`       | date-time or null                                                                                                    | The last scheduled change.                                                                                                                              |
| `version`            | integer                                                                                                              |                                                                                                                                                         |
| `createdAt`          | date-time                                                                                                            |                                                                                                                                                         |
| `updatedAt`          | date-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

| Code                                                                                                                 | HTTP status | When                                                                                                                                                | What to do                                                                                                                                                      |
| -------------------------------------------------------------------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [`validation_failed`](https://ownrecord.io/docs/reference/errors.md#validation_failed)                               | 400         | A 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_found`](https://ownrecord.io/docs/reference/errors.md#resource_not_found)                             | 404         | No 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_invalid`](https://ownrecord.io/docs/reference/errors.md#subscription_migration_invalid)     | 400         | The 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_large`](https://ownrecord.io/docs/reference/errors.md#subscription_migration_too_large) | 400         | More 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_conflict`](https://ownrecord.io/docs/reference/errors.md#version_conflict)                                 | 409         | A matching subscription changed at the same time; retry.                                                                                            | Fetch the object again and retry the change.                                                                                                                    |
| [`permission_denied`](https://ownrecord.io/docs/reference/errors.md#permission_denied)                               | 403         | The 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](https://ownrecord.io/docs/reference.md#common-errors): [`api_version_unsupported`](https://ownrecord.io/docs/reference/errors.md#api_version_unsupported), [`invalid_json`](https://ownrecord.io/docs/reference/errors.md#invalid_json), [`unsupported_media_type`](https://ownrecord.io/docs/reference/errors.md#unsupported_media_type), [`authentication_required`](https://ownrecord.io/docs/reference/errors.md#authentication_required), [`api_key_invalid`](https://ownrecord.io/docs/reference/errors.md#api_key_invalid), [`scope_required`](https://ownrecord.io/docs/reference/errors.md#scope_required), [`idempotency_key_required`](https://ownrecord.io/docs/reference/errors.md#idempotency_key_required), [`idempotency_key_reused`](https://ownrecord.io/docs/reference/errors.md#idempotency_key_reused), [`idempotency_in_progress`](https://ownrecord.io/docs/reference/errors.md#idempotency_in_progress), [`approval_required`](https://ownrecord.io/docs/reference/errors.md#approval_required), [`approval_rejected`](https://ownrecord.io/docs/reference/errors.md#approval_rejected), [`approval_expired`](https://ownrecord.io/docs/reference/errors.md#approval_expired), [`stale_approval`](https://ownrecord.io/docs/reference/errors.md#stale_approval), [`approval_policy_denied`](https://ownrecord.io/docs/reference/errors.md#approval_policy_denied), [`rate_limited`](https://ownrecord.io/docs/reference/errors.md#rate_limited), [`internal_error`](https://ownrecord.io/docs/reference/errors.md#internal_error).
