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_subscriptionstotrue, it moves subscriptions from the source price to the new price. The source price is thesource_price_idyou 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.
Migrate a price
API
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
Response
The response is HTTP 200 even when some items fail. Check errors.
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_idneedsmigrate_subscriptionsset totrue. If you give a source price andmigrate_subscriptionsisfalse, the item fails.- When
migrate_subscriptionsistrue:- 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 fromtarget_price_id.
- When
migrate_subscriptionsisfalse, 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_subscriptionsistrue, the product has no default price and you give nosource_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.
To move subscriptions to a different billing interval (for example monthly to yearly), use 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 returns422and 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 same400. - 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, 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.
A dry run sends no events.
Example: lower a plan from 440 a month
Your “Pro” plan (prod_pro) has a default price of 440 a month.
-
Create a new recurring price on
prod_pro: $440, USD, intervalmonth, interval count 1. Say it gets the IDprice_440. -
Run a dry run to see how many subscription items will move and to catch any errors:
Check
results[0].subscriptions_migratedanderrors. Nothing has changed yet. -
Send the same request with
"dry_run": false.
After the migration:
price_440is the default price ofprod_pro.- Subscriptions on
price_449in the states listed in the table above (active, trialing, paused, past-due and others) now point atprice_440. Cancelled subscriptions do not change. No one is charged now. - On each moved subscription’s next invoice, the moved item bills 449.
- A past-due subscription’s open 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.