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 lists the plan versions the POST accepts. It only lists plan versions currently inside their effective window (effective_from / effective_to) that are offered to existing customers, fit the building’s speed tier and match its bulk offer — the POST applies the same checks and refuses anything else. The list is a snapshot: when the building’s tier or bulk configuration cannot be read, it is not filtered, and the POST still refuses with 422 (detail.code = "speed_tier" or "bulk_plan"). Only can_change_today: false guarantees a refusal outright. A plan change never asks whether your provider still covers the building. It asks about the building’s plan selection only when one exists: with none on the building, its division or its province, every plan of yours is allowed; a selection that leaves the plan out is a 422 with detail.code = "plan_not_offered", and the plan is missing from eligible_plan_versions.

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. One change to a line runs at a time: a second POST for the same subscription while the first is still running answers 409 {"detail": {"code": "action_in_progress", "message": "…"}} and changes nothing; once the first has finished, a second change the same day meets the daily limit. The same holds for kurnl’s own change-plan route and every other change to the line (a cancellation, a suspension).

Operator discounts

A kurnl operator discount on the line goes with it to the new plan: it is re-priced on the new plan’s list price and its remaining months keep running. Kurnl’s wholesale reduction follows the new price. A provider’s own campaign still ends on a plan change. The customer portal’s plan-change preview reports a continuing operator discount as campaign_continues (name, promo_price_monthly); this endpoint’s preview quotes no price.

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).