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_typeisUPGRADEwhen the new plan’s retail price is greater than or equal to the old one’s, otherwiseDOWNGRADE.effective_atis the moment kurnl applied the change (UTC, ISO 8601) — this is also always “now”; there is no future-datedeffective_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_oncolumns) — 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_requiredistrue, 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.
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
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.
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_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_changednotification an operator-triggered change would send, if that automation is enabled for your provider. The partner API does not suppress it.
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).