When to use this
Use CKO-04 when:- Your customers buy on your own website — your funnel, your cart, your payment provider
- You are an incumbent or existing ISP bringing your own subscribers to the kurnl network
- You bill your customers yourself — or you let kurnl bill them from the card your storefront saved (see Who bills)
- You want to provision subscribers programmatically from your own back-office system without a marketplace flow
How it works
Three calls from your storefront, then one to provision:- Creates the subscriber record in kurnl (or reuses an existing account by email)
- Creates the subscription
- Either provisions the switch port via SSH (VLAN, bandwidth, optional MAC binding),
or raises an install job for a technician — decided by the building’s line type,
not by you. The response’s
routesays which.
A back-office migration where you already hold port mappings skips the first
three calls and sends a
location_hash directly — see Naming the premise below.Making the call
job_id is null, route is "install", and the
subscription is PENDING_INSTALL until a technician calls
POST /provisioning/confirm-install.
The port is provisioned asynchronously. Poll the job or wait for the provisioning.completed webhook.
Retries are safe (idempotent). A repeat call for the same (subscriber email, plan version, location) triple returns 200 with the existing subscription instead of creating a duplicate — message is "Provisioning already in progress" (a job is still running) or "Subscription already exists (idempotent replay)", and job_id may be an empty string on a replay, so treat it as optional. The same subscriber + plan at a different location provisions a second line normally. A SUSPENDED subscription counts as existing.
Rate limit: 120 requests/minute per API key.
Errors
Naming the premise
Every order has to say where. There are two ways, and which one you can use depends on what you know about the customer.A unit — for a storefront (delivery_mode: "home_drop")
A shopper typing their address into your site has no way to know their switch port,
and neither do you. Ask them for their address and suite instead:
POST /serviceability/checkwith the address → abuilding_id.GET /serviceability/units?building_id=…→ the suites, each withavailable.POST /provisioning/provider-initiatedwithdelivery_mode: "home_drop"and the chosenunit_id.
available
rather than anything else you may have cached about a unit.
A port — for a migration (delivery_mode: "direct", the default)
A location_hash identifies a physical switch port. You get one of two ways:
- From your provisioning records — if you mapped your subscriber addresses to kurnl location hashes during onboarding
- From the kurnl dashboard — look up the port in Dashboard → Network and copy the location hash
Provision or install — the building decides
delivery_mode states what you have. The building’s line type decides what
happens, and a request that does not fit it is refused with 422 rather than
half-completed:
Read
route in the response (provision or install) and branch your confirmation
screen on it — never on the presence of job_id, which is also empty on an
idempotent replay.
Who bills: you, or kurnl
By default you bill the subscriber. kurnl creates no Stripe records and no invoices for the order — it tracks the subscription for network management (deprovisioning on cancellation, reporting) and never charges anyone. That isbilling.owner: "partner", and it is what every order without a billing block gets.
If your storefront has already put the customer into your Stripe account and saved a card there, you can
hand the billing to kurnl instead. Add a billing block at the top level of the order, next to
plan_version_id:
What your storefront has to do
- Create the customer and save the card in your Stripe account — the one connected to kurnl. kurnl never creates a customer or a payment method for a CKO-04 order.
- Save the card for use without the customer present: a SetupIntent with
usage: "off_session"(or a payment withsetup_future_usage: "off_session"). kurnl cannot check this — a card saved without it fails its first monthly charge with an authentication requirement, and the customer is then e-mailed a payment link. - Do not charge the first period yourself. kurnl does (below).
What kurnl checks before it writes anything
With your key, kurnl verifies that it will be able to bill later — and refuses the order otherwise. On a refusal nothing is created; fix the cause and resend.detail starts with the code, followed by a human-readable reason — match on the prefix.
What kurnl does then
kurnl adopts the customer and its default payment method (else the first attached card) onto the subscriber, recordsbilling_owner: "kurnl" on the subscription, and bills it exactly the way it bills a marketplace
order:
- At activation — when the port is live (
route: "provision"), or when the technician completes the install (route: "install") — kurnl issues the first invoice: the plan’s setup fee plus the current month prorated from that day to month end, and charges the saved card. - Every following month the recurring invoice falls on
billing_dayand is charged to the same card. AutoPay is on. - The subscriber sees the card, AutoPay and every invoice in the self-service portal; the provider’s lifecycle e-mails (invoice issued, payment failed, reminders) go out; and a failed charge enters the provider’s dunning and grace policy, which can end in auto-cutoff. If you hand billing to kurnl, this is what your customer experiences — say so in your own terms.
Choose per order. One storefront can send partner-billed and kurnl-billed orders side by side; the choice is
recorded on the subscription and does not change afterwards. Bulk batches take the same block per item — see
Bulk Provisioning.
Cancellation
When a subscriber’s service ends, call the subscription cancel endpoint so kurnl can deprovision the port:
This marks the subscription
CANCELLED and sets canceled_at and ended_at (the response echoes both). ended_at is exclusive — service is off on that day, from midnight in the provider’s time zone (the value is that midnight as a UTC instant). There is no minimum term: any date from today on is accepted. The port itself is not reset at call time: the deprovisioner resets it to the captive-portal VLAN once ended_at is reached. Service stays up until then.
GET /api/v1/subscription/{subscription_id}/cancel/preview?end_date=YYYY-MM-DD (same JWT) is a read-only quote of what a cancellation on that date would record — the effective date, the calendar month it lands in, and, for kurnl-billed subscriptions, whether the month has already been invoiced (a credit note for the unused days plus every open credit swept into the refund) or has not (a final invoice for the days served, minus open credits). For a partner-billed CKO-04 subscription the money lines are always empty; a kurnl-billed one shows the same lines as a marketplace subscription.
The cancellation endpoint is on the internal API surface (
/api/v1) and requires dashboard JWT authentication, not an API key. If you need programmatic cancellation from your back-office system, use a service account JWT obtained from the dashboard login flow. Alternatively, cancellations can be initiated from Dashboard → Subscriptions.