> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.paymentkit.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.paymentkit.com/_mcp/server.

# Change a price

You cannot change the amount of a price after you create it. To change what a plan costs, create a new price on the same product, then call the bulk-migrate endpoint. It does two things for each product you list:

* It makes the new price the product's default price.
* If you set `migrate_subscriptions` to `true`, it moves subscriptions from the source price to the new price. The source price is the `source_price_id` you send. If you send none, it is the product's current default price.

# What happens to existing subscriptions

* **No charge at migration.** The endpoint creates no invoice and no proration.
* **The new price applies from each subscription's next invoice.** That is the next renewal, the first paid invoice after a trial, or the invoice when a paused subscription resumes.
* **Metered prices bill the current period at the new price.** Metered usage is billed at the end of the period, at the price the item is on then. So usage recorded before the migration in the current period is also billed at the new price. The new price must use the same meter (see the validation rules).
* **Invoices that already exist keep the old amount.** For example, an open invoice on a past-due subscription still asks for the old amount.
* **Only active items on the source price move.** An item that is scheduled to start at the end of the period is not active yet, so it keeps its price. When it starts, it bills the old price. To change it, update the subscription's items.
* **A scheduled plan change still runs.** If a subscription has a plan change scheduled for the end of the period, that change runs before the renewal. Items it changes get the price it was scheduled with, which can be the old price. Items it does not change keep the new price.

| Subscription state              | Result                                                                          |
| ------------------------------- | ------------------------------------------------------------------------------- |
| `active`, `active_unsettled`    | Moved. The next renewal bills the new price.                                    |
| `trialing`                      | Moved. The first paid invoice bills the new price.                              |
| `paused`                        | Moved. The subscription bills the new price when it resumes.                    |
| `past_due`                      | Moved. Open invoices keep the old amount. The next renewal bills the new price. |
| `incomplete`, `scheduled`       | Moved.                                                                          |
| `cancelled` and any other state | Not moved.                                                                      |

# Migrate a price

#### API

```bash
curl -X POST https://app.paymentkit.com/api/{account_id}/prices/bulk-migrate \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
  "dry_run": false,
  "migrations": [
    {
      "product_id": "prod_abc123",
      "target_price_id": "price_new440",
      "migrate_subscriptions": true
    }
  ]
}'
```

To check the result first, send the same request with `"dry_run": true`. A dry run runs every check and changes nothing. In each result, `subscriptions_migrated` shows how many subscription items would move. After a real run, it shows how many moved.

A dry run plans the items in order, as the real run does. If two items name the same product, the second item uses the default price and the subscription moves that the first item would make.

## Request fields

| Field                                | Required | Description                                                                                                                                                                                                                                                                                                                            |
| ------------------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `migrations`                         | Yes      | List of products to migrate. At least one. Each item is checked and saved on its own.                                                                                                                                                                                                                                                  |
| `migrations[].product_id`            | Yes      | The product to migrate.                                                                                                                                                                                                                                                                                                                |
| `migrations[].target_price_id`       | Yes      | The new price. It must belong to the product and it must be active. It becomes the product's default price.                                                                                                                                                                                                                            |
| `migrations[].migrate_subscriptions` | No       | Default `false`. When `true`, active subscription items on the source price move to the target price.                                                                                                                                                                                                                                  |
| `migrations[].source_price_id`       | No       | The price to move subscriptions off. Default: the product's current default price. It must belong to the same product and it must not be the target price. Use it when subscriptions are on a price that is not the default. Requires `migrate_subscriptions: true`. The item fails if you set it with `migrate_subscriptions: false`. |
| `dry_run`                            | No       | Default `false`. When `true`, the endpoint runs every check and returns the results, including how many subscriptions would move, but changes nothing.                                                                                                                                                                                 |

## Response

The response is HTTP 200 even when some items fail. Check `errors`.

```json
{
  "dry_run": false,
  "total_requested": 1,
  "successful": 1,
  "failed": 0,
  "results": [
    {
      "product_id": "prod_abc123",
      "old_default_price_id": "price_old449",
      "new_default_price_id": "price_new440",
      "source_price_id": "price_old449",
      "subscriptions_migrated": 1250
    }
  ],
  "errors": []
}
```

| Field                                     | Description                                                                                                         |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| `dry_run`                                 | `true` when the request was a dry run, so nothing was changed.                                                      |
| `total_requested`, `successful`, `failed` | Counts of migration items.                                                                                          |
| `results[].old_default_price_id`          | The product's default price before the migration. `null` if it had none.                                            |
| `results[].source_price_id`               | The price the subscriptions moved off. `null` when `migrate_subscriptions` is `false` or there was no source price. |
| `results[].subscriptions_migrated`        | Number of subscription items moved. On a dry run, the number that would move.                                       |
| `errors[]`                                | `product_id`, `target_price_id` and an `error` message for each failed item.                                        |

# Validation rules

Each item is checked before anything changes. An item that fails a check is listed in `errors` and changes nothing.

* The product and the prices must exist in your account. An ID from another account is reported as "not found".
* The target price (and the source price, if given) must belong to the product.
* The target price must be active.
* `source_price_id` needs `migrate_subscriptions` set to `true`. If you give a source price and `migrate_subscriptions` is `false`, the item fails.
* When `migrate_subscriptions` is `true`:
  * The target price must be recurring.
  * The source price must be recurring too. This includes the product's current default price when you give no `source_price_id`. If the default is a one-time price, the item fails with "Source price is not recurring".
  * The target price must match the source price: same currency, same billing interval (for example `month`), same interval count, and same usage type (licensed or metered). A metered target must use the same meter as the source.
  * If you give `source_price_id`, it must be different from `target_price_id`.
* When `migrate_subscriptions` is `false`, the price checks are only the active check: currency, interval and the recurring check do not apply. No subscription changes, so you can set a one-time price as the default.
* When `migrate_subscriptions` is `true`, the product has no default price and you give no `source_price_id`, there is nothing to move. Of the price checks, only the active and recurring checks apply.

If you give no `source_price_id`, the source price is the product's current default price. If the target price is already the default price, no subscriptions move.

> **Note**
>
> To move subscriptions to a different billing interval (for example monthly to yearly), use [subscription change requests](/guides/billing/subscriptions/subscription-change-requests) instead. This endpoint does not change a subscription's billing interval, so it rejects those targets.

# Limits

One request has two limits:

* **Migration items:** at most 500 in `migrations`. A request with more returns `422` and nothing changes. Each item counts, even one that moves no subscriptions.
* **Subscription items:** at most 7,500 moved in total. PaymentKit adds up the items that all migration items in the request would move, before it changes anything.

When the subscription-item total is more than 7,500:

* The request returns `400`. Nothing changes and no events are sent. A dry run returns the same `400`.
* If several migration items add up to more than 7,500, the message gives the limit and the total counted up to the first item over it (the full total can be higher). Split the migration items into smaller requests. If you split the items of one product across requests, give each item an explicit `source_price_id`. Without it, a later request reads the source as the product's current default, which an earlier request may already have changed to the new price, and then it moves nothing.
* If one migration item (one source price) alone would move more than 7,500 items, the message names its product. The items on one source price cannot be split into smaller requests, so bulk-migrate cannot move them. This is not supported yet. A product with items on several prices can still be split: send one item per source price, each with its `source_price_id`.

Rarely, subscriptions are added to a source price while a request runs, after the total was checked. If an item would then take the request over 7,500, the request still returns `200`. The items before it are saved. That item and every item after it are listed in `errors` with a "Not applied" message, and nothing changed for them. Send them again in a new request.

# Each item is all or nothing

For one item, the default price switch and all subscription moves are saved together. If something fails part way, none of that item's changes are saved and the item is listed in `errors`. Each item is saved on its own, so a later failure does not undo an item that already succeeded.

When an item fails, PaymentKit does not apply the later items for the same product in that request. Each one is listed in `errors` with "Not applied: an earlier item for this product failed. Fix it and send all the items for this product again." A dry run gives the same result. This rule prevents a later item from changing the default price that the failed item needs. Items for other products still run.

# Events

For each subscription that moves, PaymentKit sends a `customer.subscription.updated` event. In the [Events API](/guides/integration/events), the event's `metadata` has `action: "items_changed"`, with `previous_items` and `items` showing the item's price before and after. The product also gets a `product.updated` event for the new default price. See [webhooks](/guides/integration/webhooks).

A dry run sends no events.

# Example: lower a plan from $449 to $440 a month

Your "Pro" plan (`prod_pro`) has a default price of $449 a month (`price_449`). You want new and existing subscribers to pay $440 a month.

1. Create a new recurring price on `prod_pro`: \$440, USD, interval `month`, interval count 1. Say it gets the ID `price_440`.
2. Run a dry run to see how many subscription items will move and to catch any errors:

   ```bash
   curl -X POST https://app.paymentkit.com/api/{account_id}/prices/bulk-migrate \
   -H "Authorization: Bearer sk_live_..." \
   -H "Content-Type: application/json" \
   -d '{
     "dry_run": true,
     "migrations": [
       {"product_id": "prod_pro", "target_price_id": "price_440", "migrate_subscriptions": true}
     ]
   }'
   ```

   Check `results[0].subscriptions_migrated` and `errors`. Nothing has changed yet.
3. Send the same request with `"dry_run": false`.

After the migration:

* `price_440` is the default price of `prod_pro`.
* Subscriptions on `price_449` in the states listed in the table above (active, trialing, paused, past-due and others) now point at `price_440`. Cancelled subscriptions do not change. No one is charged now.
* On each moved subscription's next invoice, the moved item bills $440 (unless a scheduled plan change changes that item, see above). Quantity, discounts, tax and other items apply as before. An item scheduled to start at the end of the period did not move and still bills $449.
* A past-due subscription's open $449 invoice still asks for $449.

If some subscribers are on another \$449 price that is not the default (for example `price_449_legacy`), send a second item for the same product with `"source_price_id": "price_449_legacy"` to move them too. You can send both items in one request. Put the item without `source_price_id` first: after an item runs, the product's default price is the new price, so a later item without `source_price_id` has no subscriptions to move. If you send the items in separate requests, give each one an explicit `source_price_id` (here `price_449` and `price_449_legacy`), so the order does not matter.