Preview a subscription change
The subscriptions.preview_change operation: POST /api/v1/subscriptions/{id}/preview_change in the REST API and the subscriptions_preview_change tool in the MCP server.
/api/v1/subscriptions/{id}/preview_changeShows exactly what subscriptions.update would do with the same item changes, proration behaviour, timing and
promotion code, without changing anything: whether it is an upgrade, a downgrade or lateral, when it applies,
and how it is settled (settlement): the invoice issued now (invoice: lines, prorations, discounts, tax at
today's rates, the credit balance applied, the amount due; without a number until it is issued), the pending
items for the next invoice, the credit to the customer's credit balance, or the scheduled change. Also the
subscription as it would be and its monthly recurring amount before and after.
The preview runs the change's own code in a transaction that is rolled back, so it matches the real change as long as nothing changes in between (the time moves on: proration is exact to the second).
At a glance
- REST:
POST /api/v1/subscriptions/{id}/preview_change - MCP tool:
subscriptions_preview_change - Operation ID:
subscriptions.preview_change - Scope: Brand
- Permission:
subscriptions:read - Risk tier:
read - Dry run: Not supported
- Idempotency:
Idempotency-Keyrequired - Modes: Test and live
- Authentication: API key with the
subscriptions:readscope - Since: M5
Request
Path parameters
| Field | Type | Description |
|---|---|---|
id | sub_… ID |
Body
A JSON object (Content-Type: application/json):
| Field | Type | Required | Description |
|---|---|---|---|
items | array of object | No | Item changes: { id, priceId } swaps a price, { id, quantity } changes seats, { priceId, quantity } adds an item, { id, deleted: true } removes one. Items not named stay. |
items[].id | si_… ID | No | The item to change or remove (si_…); leave out to add one. |
items[].priceId | price_… ID | No | The new price of the item, or of a new item. |
items[].quantity | integer (1 to 1000000) | No | Seats or units of a licensed price (1 to 1,000,000). Metered prices take no quantity. |
items[].deleted | boolean | No | Remove the item (with id). |
prorationBehavior | one of create_prorations, always_invoice, none | No | create_prorations (credit unused time and charge the remaining time on the next invoice; a net credit goes to the customer credit balance), always_invoice (invoice the proration now) or none. |
effective | one of now, period_end | No | now, or at the end of the current period (period_end, stored as pendingUpdate). Default: upgrades and interval changes now with always_invoice, downgrades at the period end. |
paymentBehavior | one of allow_incomplete, pending_if_incomplete | No | For a change invoiced now: allow_incomplete (default; the change applies and a failed payment goes to dunning) or pending_if_incomplete (the old items stay until the invoice is paid). |
couponId | cpn_… ID | No | Apply a coupon to the subscription. |
promotionCode | string | No | A promotion code as the customer typed it (case doesn't matter). |
Response
200 OK with a JSON object:
| Field | Type | Description |
|---|---|---|
object | "subscription_change_preview" | |
subscriptionId | sub_… ID | |
direction | one of upgrade, downgrade, lateral | |
intervalChange | boolean | |
effective | one of now, period_end | |
prorationBehavior | one of create_prorations, always_invoice, none | |
settlement | one of invoice_now, pending_items, credit_balance, pending_update, none | invoice_now: invoice is issued now; pending_items: the prorations wait for the next invoice; credit_balance: a net credit goes to the customer credit balance; pending_update: the change waits (period end or payment); none: nothing to bill. |
invoice | object or null | The exact invoice the change issues now (its number is taken when it is issued). |
pendingItems | array of object | |
pendingItems[].description | string | |
pendingItems[].amount | money | |
pendingItems[].periodStart | date-time or null | |
pendingItems[].periodEnd | date-time or null | |
credit | money or null | What goes to the customer credit balance. |
subscription | object | The subscription as it would be after the change. |
subscription.id | sub_… ID | |
subscription.object | "subscription" | |
subscription.livemode | boolean | |
subscription.entityId | ent_… ID | |
subscription.brandId | brd_… ID | |
subscription.customerId | cus_… ID | |
subscription.status | one of incomplete, incomplete_expired, trialing, active, past_due, unpaid, paused, canceled | incomplete (waiting for the first payment, 23 hours), trialing, active, past_due, unpaid, paused, canceled or incomplete_expired (spec §16.1). |
subscription.items | array of object | |
subscription.items[].id | si_… ID | |
subscription.items[].priceId | price_… ID | |
subscription.items[].productId | prod_… ID | |
subscription.items[].quantity | integer | |
subscription.items[].createdAt | date-time | |
subscription.currency | string | |
subscription.mrr | money or null | The items' monthly recurring amount (licensed items, before discounts and tax). |
subscription.collectionMethod | one of charge_automatically, send_invoice | |
subscription.daysUntilDue | integer or null | |
subscription.defaultPaymentMethodId | pm_… ID or null | |
subscription.billingCycleAnchor | date-time | |
subscription.currentPeriodStart | date-time | |
subscription.currentPeriodEnd | date-time | |
subscription.trialStart | date-time or null | |
subscription.trialEnd | date-time or null | |
subscription.trialEndBehavior | one of convert, cancel_if_no_payment_method, pause_if_no_payment_method | |
subscription.cancelAtPeriodEnd | boolean | |
subscription.cancelAt | date-time or null | |
subscription.canceledAt | date-time or null | |
subscription.endedAt | date-time or null | |
subscription.cancellation | object or null | |
subscription.pause | object or null | |
subscription.discountIds | array of disc_… ID | |
subscription.pendingUpdate | object or null | |
subscription.latestInvoiceId | inv_… ID or null | |
subscription.incompleteExpiresAt | date-time or null | |
subscription.testClockId | clk_… ID or null | |
subscription.dunning | object or null | Dunning (§16.10) while past due or unpaid; null otherwise. |
subscription.usageThreshold | money or null | Unbilled metered usage worth this much is invoiced at once (spec §16.7). |
subscription.entitlementGraceUntil | date-time or null | |
subscription.metadata | object | |
subscription.version | integer | |
subscription.createdAt | date-time | |
subscription.updatedAt | date-time | |
mrrBefore | money | |
mrrAfter | money | |
mrrDelta | money |
Examples
What upgrading to Pro now costs
POST /api/v1/subscriptions/sub_01m4tx5nj8emwbyf7y9vcf47z5/preview_change
Authorization: Bearer or_test_…
Idempotency-Key: 3f1c9a52-7d0e-4b8f-9a61-0c2d5e8b7f14
Content-Type: application/json
{
"items": [
{
"id": "si_01m4tx5nj8emwbyf7y9vcf47za",
"priceId": "price_01m4tx5nj8emwbyf7y9vcf47y3"
}
]
}
Response (200):
{
"object": "subscription_change_preview",
"subscriptionId": "sub_01m4tx5nj8emwbyf7y9vcf47z5",
"direction": "upgrade",
"intervalChange": false,
"effective": "now",
"prorationBehavior": "always_invoice",
"settlement": "invoice_now",
"invoice": {
"id": "inv_01m4tx5nj8emwbyf7y9vcf47yk",
"object": "invoice",
"livemode": false,
"entityId": "ent_01m4tx5nj8emwbyf7y9vcf47x2",
"brandId": "brd_01m4tx5nj8emwbyf7y9vcf47x4",
"number": null,
"numbering": null,
"customerId": "cus_01m4tx5nj8emwbyf7y9vcf47ym",
"subscriptionId": null,
"collectionMethod": "send_invoice",
"kind": "invoice",
"status": "open",
"language": "en",
"issuedAt": "2026-10-20T09:00:00.000Z",
"issueDate": "2026-10-20",
"taxPoint": "2026-10-20T09:00:00.000Z",
"taxPointDate": "2026-10-20",
"daysUntilDue": 14,
"dueDate": "2026-11-03",
"seller": {
"entityId": "ent_01m4tx5nj8emwbyf7y9vcf47x2",
"legalName": "Acme Digital d.o.o.",
"tradingName": null,
"address": {
"line1": "Ilica 1",
"line2": null,
"city": "Zagreb",
"postalCode": "10000",
"region": null,
"country": "HR"
},
"companyRegistryId": "080000001",
"taxIds": [
{
"type": "eu_vat",
"value": "HR12345678901",
"regimeId": null
}
],
"brand": {
"brandId": "brd_01m4tx5nj8emwbyf7y9vcf47x4",
"name": "Genno",
"primaryColor": "#0E6B5C",
"accentColor": null,
"supportEmail": "help@genno.example",
"supportUrl": null
},
"footerText": null
},
"buyer": {
"type": "b2b",
"customerId": "cus_01m4tx5nj8emwbyf7y9vcf47ym",
"name": "Alpen Apps GmbH",
"email": "billing@alpen.example",
"address": null,
"country": "AT",
"taxId": {
"type": "eu_vat",
"value": "ATU00000000",
"country": "AT"
}
},
"currency": "EUR",
"lines": [
{
"id": "1",
"description": "Implementation workshop (days)",
"productId": "prod_01m4tx5nj8emwbyf7y9vcf47yr",
"priceId": null,
"quantity": 2,
"taxCategoryId": "consulting_remote",
"taxBehavior": "exclusive",
"unitAmount": {
"amount": "600000000",
"currency": "EUR"
},
"net": {
"amount": "1200000000",
"currency": "EUR"
},
"tax": {
"ratePpm": "0",
"amount": {
"amount": "0",
"currency": "EUR"
},
"jurisdictionId": "AT",
"regimeId": "EU_OSS_UNION",
"decision": "reverse_charge",
"rateKind": null,
"rateId": null,
"pendingRegistration": false
},
"gross": {
"amount": "1200000000",
"currency": "EUR"
},
"kind": "invoice_item",
"period": null,
"subscriptionItemId": null,
"invoiceItemId": "ii_01m4tx5nj8emwbyf7y9vcf47ys",
"discount": null,
"meteredQuantity": null
}
],
"totals": {
"net": {
"amount": "1200000000",
"currency": "EUR"
},
"tax": {
"amount": "0",
"currency": "EUR"
},
"gross": {
"amount": "1200000000",
"currency": "EUR"
},
"taxByRate": [
{
"ratePpm": "0",
"jurisdictionId": "AT",
"regimeId": "EU_OSS_UNION",
"decision": "reverse_charge",
"net": {
"amount": "1200000000",
"currency": "EUR"
},
"tax": {
"amount": "0",
"currency": "EUR"
}
}
]
},
"taxCurrencyTotals": null,
"creditApplied": {
"amount": "200000000",
"currency": "EUR"
},
"amountDue": {
"amount": "1000000000",
"currency": "EUR"
},
"notes": [
{
"code": "reverse_charge",
"text": "Prijenos porezne obveze (reverse charge) – čl. 17. st. 1. Zakona o PDV-u"
}
],
"fieldCheck": {
"invoicingRuleIds": [],
"missing": ["buyer_address"],
"unchecked": []
},
"pendingRegistration": false,
"paymentIds": [],
"creditNoteIds": [],
"ledgerTxnIds": ["txn_01m4tx5nj8emwbyf7y9vcf47yn"],
"quoteId": "tq_01m4tx5nj8emwbyf7y9vcf47xr",
"quoteIds": ["tq_01m4tx5nj8emwbyf7y9vcf47xr"],
"pdf": {
"status": "pending",
"fileId": null,
"sha256": null,
"size": null,
"templateVersion": null,
"renderedAt": null,
"url": "/api/v1/invoices/inv_01m4tx5nj8emwbyf7y9vcf47yk/pdf"
},
"finalizedAt": "2026-10-20T09:00:00.000Z",
"paidAt": null,
"payment": null,
"voidedAt": null,
"markedUncollectibleAt": null,
"emailedAt": null,
"sentAt": null,
"draft": null,
"metadata": {
"contract": "ENT-2026-07"
},
"version": 2,
"createdAt": "2026-10-20T08:30:00.000Z",
"updatedAt": "2026-10-20T09:00:00.000Z"
},
"pendingItems": [],
"credit": null,
"subscription": {
"id": "sub_01m4tx5nj8emwbyf7y9vcf47z5",
"object": "subscription",
"livemode": false,
"entityId": "ent_01m4tx5nj8emwbyf7y9vcf47zm",
"brandId": "brd_01m4tx5nj8emwbyf7y9vcf47x4",
"customerId": "cus_01m4tx5nj8emwbyf7y9vcf47z4",
"status": "active",
"items": [
{
"id": "si_01m4tx5nj8emwbyf7y9vcf47za",
"priceId": "price_01m4tx5nj8emwbyf7y9vcf47y3",
"productId": "prod_01m4tx5nj8emwbyf7y9vcf47y0",
"quantity": 3,
"createdAt": "2026-10-13T12:00:00.000Z"
}
],
"currency": "EUR",
"mrr": {
"amount": "147000000",
"currency": "EUR"
},
"collectionMethod": "charge_automatically",
"daysUntilDue": null,
"defaultPaymentMethodId": "pm_01m4tx5nj8emwbyf7y9vcf47zd",
"billingCycleAnchor": "2026-10-13T12:00:00.000Z",
"currentPeriodStart": "2026-10-13T12:00:00.000Z",
"currentPeriodEnd": "2026-11-13T12:00:00.000Z",
"trialStart": null,
"trialEnd": null,
"trialEndBehavior": "convert",
"cancelAtPeriodEnd": false,
"cancelAt": null,
"canceledAt": null,
"endedAt": null,
"cancellation": null,
"pause": null,
"discountIds": [],
"pendingUpdate": null,
"latestInvoiceId": "inv_01m4tx5nj8emwbyf7y9vcf47zn",
"incompleteExpiresAt": null,
"testClockId": null,
"dunning": null,
"usageThreshold": null,
"entitlementGraceUntil": null,
"metadata": {
"crm_account": "A-1042"
},
"version": 2,
"createdAt": "2026-10-13T12:00:00.000Z",
"updatedAt": "2026-10-13T12:00:00.000Z"
},
"mrrBefore": {
"amount": "87000000",
"currency": "EUR"
},
"mrrAfter": {
"amount": "147000000",
"currency": "EUR"
},
"mrrDelta": {
"amount": "60000000",
"currency": "EUR"
}
}
Errors
| Code | HTTP status | When | What to do |
|---|---|---|---|
validation_failed | 400 | A field is malformed or out of range. | Fix the fields listed in details.issues and send the request again. |
resource_not_found | 404 | No subscription (or customer, price, coupon) with this ID is visible to the request. | Check the ID and that your credentials are scoped to the right organization, entity, brand and mode. |
subscription_status_invalid | 409 | The subscription's status doesn't allow this (spec §16.1): see details.reason. | Check details.status: items change only while trialing, active or past due; pausing needs an active subscription and resuming a paused one; reactivating needs a cancellation scheduled for the period end; canceled and expired subscriptions don't change. |
subscription_item_not_found | 404 | An item id isn't one of the subscription's items. | Read the subscription and use one of its items[].id (si_…); to add a new price, leave out id. |
subscription_price_invalid | 400 | A new price isn't recurring, is archived, is in another currency, or its interval doesn't combine. | Use active recurring prices of one currency whose intervals are whole multiples of each other (monthly with yearly, weekly with 2-weekly). details.reason says what's wrong and param names the item. |
subscription_meter_conflict | 409 | Another live item of the customer already bills this meter. | Change the existing subscription's metered item instead, or cancel it first. A customer has at most one active metered item per meter. |
subscription_change_invalid | 400 | A change waits for its invoice to be paid, or pending_if_incomplete was asked for a send_invoice subscription. | Check details.reason and adjust the request. |
coupon_not_redeemable | 409 | The coupon is archived, expired or used up. | Check details.reason: reactivate an archived coupon, or create a new coupon when it expired or reached its redemption limit. |
promotion_code_not_redeemable | 409 | The promotion code can't be used (details.reason). | Check details.reason and tell the customer: the code may have expired, be used up, belong to another customer, need a larger purchase or be for first-time customers only. |
discount_already_applied | 409 | The coupon is already applied to this subscription. | End the existing discount first (discounts.delete), or apply a different coupon. The same coupon doesn't stack with itself. |
registration_required | 422 | An invoice issued now can't be taxed: the seller isn't registered where the customer is (a new sale). | Register the entity for the regime named in details.regime (see its prerequisites and deadline), or arm the jurisdiction once the prerequisites are done. The customer can join the waitlist meanwhile. |
jurisdiction_blocked | 422 | The customer's location is blocked for new sales. | This jurisdiction isn't covered by usable tax knowledge for this seller. Contact support if you need to sell there. |
evidence_conflict | 422 | The customer's stored location evidence isn't enough to tax an invoice issued now (§13.3). | Collect another piece of location evidence (billing address, card country, or the customer's confirmation of their country) and quote again. |
permission_denied | 403 | You can't see this brand's subscriptions (subscriptions:read). | 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, dry_run_not_supported, rate_limited, internal_error.