BillKit/Docs Console
Subscriptions

Change a subscription's plan

Moves a subscription to a different price. BillKit prices the switch, charges the difference against the existing mandate, and applies the new plan when that payment settles.

POST/v1/subscriptions/{subscription_id}/update
Operation update_subscription · Scope subscriptions:write · Spec snapshot 2026-09-13

Parameters

ParameterDescription
subscription_idstring · pathrequiredNo description in the spec.
target_price_idstringrequiredNo description in the spec.

Returns

The subscription as it stands right now, which is still on the old price. The switch is asynchronous: this call only mints the charge. Watch for subscription.updated, and for payment.succeeded or payment.failed alongside it.

The price of the switch is a credit for the unused part of the current period, subtracted from the full new price. Preview it first if you want to show the customer the number before they commit.

A change that would charge less than one unit of currency is refused with 400 parameter_invalid. The customer has already paid through nearly all of the period, so waiting for the renewal bills them at the new rate anyway.

Test it

Change the plan, then advance a test clock past the period end and read the invoice that falls out.

Errors

Status Cause
401 Missing or invalid API key.
403 Key is missing the subscriptions:write scope.
404 No such object, or it belongs to the other mode.
422 Validation Error
429 Rate limited. Back off and retry.
Request · Node.js
// Price the switch first, so the customer sees the number.
const quote = await client.subscriptions.previewUpdate("sub_9XKp2vQ1", {
  target_price_id: "price_pro_yearly",
});

const subscription = await client.subscriptions.update("sub_9XKp2vQ1", {
  target_price_id: "price_pro_yearly",
  idempotencyKey: "swap-sub_9XKp2vQ1-to-yearly",
});

// Still on the old price here. The switch lands when the proration
// charge settles; wait for subscription.updated.
Request · Python
# Price the switch first, so the customer sees the number.
quote = client.subscriptions.preview_update(
    "sub_9XKp2vQ1",
    target_price_id="price_pro_yearly",
)

subscription = client.subscriptions.update(
    "sub_9XKp2vQ1",
    target_price_id="price_pro_yearly",
    idempotency_key="swap-sub_9XKp2vQ1-to-yearly",
)

# Still on the old price here. The switch lands when the proration
# charge settles; wait for subscription.updated.
Request · PHP
// Price the switch first, so the customer sees the number.
$quote = $client->subscriptions->previewUpdate('sub_9XKp2vQ1', 'price_pro_yearly');

$subscription = $client->subscriptions->update(
    'sub_9XKp2vQ1',
    'price_pro_yearly',
    'swap-sub_9XKp2vQ1-to-yearly', // idempotency key
);

// Still on the old price here. The switch lands when the proration
// charge settles; wait for subscription.updated.
Request · Laravel
$subscription = $user->subscription();

// Price the switch first, so the customer sees the number.
$quote = $subscription->previewSwap('price_pro_yearly');

$subscription->swap('price_pro_yearly');

// Still on the old price here. The webhook controller updates the
// local row when the proration charge settles.
Request · React
// Plan changes are a server call: they move money against a stored
// mandate. Do the swap in your own API and let the browser read the
// result back.
await fetch("/api/billing/swap", {
  method: "POST",
  body: JSON.stringify({ targetPriceId: "price_pro_yearly" }),
});
Request · curl
curl -X POST https://api.billkit.eu/v1/subscriptions/sub_9XKp2vQ1/update \
  -H "Authorization: Bearer $BILLKIT_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target_price_id":"price_9XKp2vQ1"}'
Response · 200 OK
{
  "cancel_at_period_end": false,
  "canceled_at": 1789392000,
  "cancellation_reason": "string",
  "created": 1789392000,
  "current_period_end": 0,
  "current_period_start": 0,
  "customer": {
    "country_code": "NL",
    "email": "ada@example.com",
    "id": "obj_9XKp2vQ1",
    "name": "Ada Lovelace",
    "object": "customer.summary"
  },
  "customer_id": "cus_9XKp2vQ1",
  "dunning_attempts_notified": 0,
  "dunning_last_notified_at": 1789392000,
  "dunning_max_attempts": 0,
  "id": "sub_9XKp2vQ1",
  "last_payment_at": 1789392000,
  "last_payment_id": "pay_9XKp2vQ1",
  "livemode": false,
  "mrr_cents": 1900,
  "object": "subscription",
  "payment_count": 0,
  "price": {
    "active": false,
    "amount_cents": 1900,
    "currency": "EUR",
    "id": "obj_9XKp2vQ1",
    "interval": "string",
    "object": "price.summary",
    "payment_methods": [
      "string"
    ],
    "product_id": "prod_9XKp2vQ1",
    "product_name": "Ada Lovelace",
    "trial_days": 0,
    "unit_amount_decimal": "string",
    "usage_type": "string"
  },
  "price_id": "price_9XKp2vQ1",
  "refund_eligibility": {
    "amount_cents": 1900,
    "currency": "EUR",
    "days_remaining": 0,
    "eligible": false,
    "object": "refund_eligibility",
    "reason": "string"
  },
  "renewal_state": "string",
  "serves_customer": false,
  "status": "string",
  "trial_end": 0,
  "trial_start": 0
}