Change a subscriber’s plan directly from your own dashboard or billing system, on any active subscription that belongs to your provider — not only ones an external checkout created. The change is applied immediately; there is no scheduled/timing option, because “next period” would mean kurnl’s calendar month, which is not necessarily your own billing cycle.

Endpoints

A key carrying provisioning:write already satisfies provisioning:read — the same key you use for provisioning calls works against the preview with no extra scope grant.

Change the plan

Response — 200 OK

  • change_type is UPGRADE when the new plan’s retail price is greater than or equal to the old one’s, otherwise DOWNGRADE.
  • effective_at is the moment kurnl applied the change (UTC, ISO 8601) — this is also always “now”; there is no future-dated effective_at.
  • Applying a change now clears any plan change an operator had scheduled for a later date on this subscription (kurnl’s scheduled_plan_version_id / scheduled_plan_change_on columns) — otherwise the scheduler would still apply the parked plan on its date and silently revert the one you just made.

bandwidth_updated

bandwidth_updated: false does not mean the call failed and does not mean the plan change didn’t happen. The subscription row and billing have already changed — this field only reports whether kurnl also finished re-provisioning the port to match. When it is false:
  • if manual_provisioning_required is true, the new plan is flagged for manual provisioning by design (see below) — this is the expected, common case;
  • otherwise, kurnl had no switch/port data on file for this subscription, the new plan version has no bandwidth defined, or the port-set attempt itself failed and was logged for an operator to retry.
Either way, the plan change stands. Retrying the request does not help — it will not touch the port again, and a second attempt today hits the daily limit below. If your integration polls or acts on bandwidth_updated, treat false as “an operator needs to confirm the port,” not as a reason to resubmit.

manual_provisioning_required

true when the new plan version is flagged manual_provisioning — kurnl skips automatic port re-provisioning entirely and opens an internal task for the provider’s operations team instead. bandwidth_updated is always false in this case, because the bandwidth step never runs.

Preview a change

Response — 200 OK

Use this before showing a customer a plan-change control, so you can disable it yourself instead of letting the customer hit a 409 on submit. It mirrors the POST’s own guards (subscription ACTIVE, once per UTC day) — whenever the preview would say can_change_today: false, the POST would refuse with the same 409 below, and vice versa. There is intentionally no price quoted here. This is an eligibility check, not the internal change-plan preview: kurnl does not bill your customers, so a kurnl-prorated price would be meaningless at best and misleading at worst on a subscription your own billing is authoritative for.
eligible_plan_versions is a strict subset of the plan versions the POST will accept, not an exact list of them. It only lists plan versions currently inside their effective window (effective_from / effective_to) — the same active-plan filter the plan-discovery endpoint uses. The POST’s own plan lookup applies no such window check, matching the platform’s existing internal change-plan route. In practice this means: a plan version an operator has retired (past its effective_to) will not appear in this list, but the POST will still accept it if you already have its id. Do not treat an id missing from this list as necessarily rejected by the POST — only can_change_today: false guarantees a refusal, and only for the reasons above.

Guardrails

The daily limit and the change-history read failure guard both exist on the preview too, in the same order, so a can_change_today: false from the preview and a 409/502 from the POST are never in disagreement for the same subscription at the same moment. (There remains a narrow read-then-act race: a second request submitted between your preview call and your POST call can still land inside the same UTC day and both count — the same race exists on kurnl’s own internal change-plan route, and closing it is out of scope for this endpoint.)

Billing

Billing follows the subscription, not the caller — but it is two independently keyed behaviours, not one, and this endpoint covers any subscription your provider owns, so they can land differently on the same subscription:
  • Recurring invoice line. Keyed on whether this subscription has a kurnl recurring invoice at all (in_recurring_invoice_id). Every subscription created through the CKO-03 external-checkout flows has none, so its invoice line is never rebuilt for a plan change — your own billing system is authoritative for the price.
  • Proration correction (a credit note now for a downgrade, a pending line on the next invoice for an upgrade). Keyed on whether the subscriber has an Invoice Ninja client on file (independent of whether there’s a recurring invoice). If one exists, the correction is issued; if not, it self-skips.
In practice: a subscription with no recurring invoice but a subscriber who does have an Invoice Ninja client on file (kurnl knows and bills that person for something else) can still receive a proration credit note or pending line, even though its recurring invoice is never touched. Which of these two things happens on a given plan change depends on how kurnl bills that particular subscription — check in_recurring_invoice_id and the subscriber’s Invoice Ninja client rather than assuming “partner-driven” means “no billing effect at all.”

Customer notification

Whether kurnl e-mails the subscriber is decided by whether kurnl holds a subscriber record for this subscription at all — not by whether the partner API or the operator dashboard made the change:
  • A subscription created through the anonymous CKO-03 flow (no subscriber record shared with kurnl) never gets an e-mail: there is no contact on file to send it to.
  • A subscription where kurnl does hold a subscriber record — including one created through the non-anonymous CKO-03 flow — gets the same subscription.plan_changed notification an operator-triggered change would send, if that automation is enabled for your provider. The partner API does not suppress it.
If your integration expects to control whether the customer is e-mailed, check whether your provider’s plan_changed automation is enabled, rather than assuming the partner API is silent by default.

Rate limits

POST .../change-plan is limited to 120 requests/minute per API key. The GET .../change-plan/preview endpoint carries no rate limit of its own.

Error codes

See also: Subscriber Management, Subscription Status (suspend/restore).