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
You never create a kurnl checkout session on this path. Sessions belong to marketplace-originated checkout (CKO-03), where kurnl resolves the address and hands the shopper to you. CKO-04 is the simplest integration path if you already have subscriber relationships and just need kurnl to activate the network port.

How it works

Three calls from your storefront, then one to provision:
The last call does everything kurnl-side:
  1. Creates the subscriber record in kurnl (or reuses an existing account by email)
  2. Creates the subscription
  3. 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 and whether the suite’s router is already online, not by you. The response’s route says which.
There is no marketplace redirect, no kurnl checkout session, and no webhook required on your side (though you can configure one to receive provisioning status). Whether kurnl also bills the subscription is your call, per order — see Who bills.
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

Successful response — 200 OK:
On the install route job_id is null, route is "install", and the subscription is PENDING_INSTALL until a technician calls POST /provisioning/confirm-install. A suite with several connections (a postal_unit on its units entry) on a first-line building is the exception: the order is accepted and held, the response carries "job_id": null and "unit_confirmation_pending": true (route stays "provision"), and nothing is provisioned — and no subscription.activated webhook fires — until the kurnl operator confirms which connection is the customer’s. See Shared suites. 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. Any other live order on the same port — another subscriber’s, or another plan version — is refused with 409 port_in_use. 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:
  1. POST /serviceability/check with the address → a building_id.
  2. GET /serviceability/units?building_id=… → the suites, each with available and cpe_ready.
  3. POST /provisioning/provider-initiated with delivery_mode: "home_drop" and the chosen unit_id.
Render unavailable suites disabled rather than hiding them, and trust available rather than anything else you may have cached about a unit. A suite with several connections is one entry (postal_unit set) and is unavailable only when all its connections are taken. cpe_ready: true means the suite’s router is already online. On a second- or third-line building such an order is provisioned at once, without a visit, so you can tell the shopper that the line starts today. Treat it as a hint, not a promise: branch your confirmation on the response’s route.

A port — for a migration (delivery_mode: "direct", the default)

A location_hash identifies a physical switch port. You get one of two ways:
  1. From your provisioning records — if you mapped your subscriber addresses to kurnl location hashes during onboarding
  2. From the kurnl dashboard — look up the port in Dashboard → Network and copy the location hash
For large migrations, use Bulk Provisioning to supply a CSV of addresses and location hashes.
The serviceability endpoints never return a location_hash, and no endpoint does. A port is knowable only from your own records, the dashboard, or a device already detected on the network — so an anonymous shopper on your site is always a home_drop order. This is the single most common wrong turn on this integration.

Provision or install — the building decides

delivery_mode states what you have. The building’s line type, and outside first line whether the suite’s router is already online, decides what happens. A request that does not fit it is refused with 422 rather than half-completed: A router counts as online when it answered within the last 15 minutes; kurnl checks this when your order arrives, and takes the install path when it cannot read the router’s state. cpe_ready on /serviceability/units shows the same state before the customer chooses, but it is a snapshot: the order can still come back as install. 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 is billing.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 with setup_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, records billing_owner: "kurnl" on the subscription, and bills it exactly the way it bills a marketplace order:
  1. 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.
  2. Every following month the recurring invoice falls on billing_day and is charged to the same card. AutoPay is on.
  3. 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.
Nothing is charged at order time. The response tells you what kurnl recorded:
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 and adds last_service_day (YYYY-MM-DD, the last day the service runs, in the provider’s time zone) and ended_immediately (true when the cancel was for today, in which case last_service_day is the day of the call). 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. For a later date the port is not reset at call time: the deprovisioner resets it to the captive-portal VLAN once ended_at is reached, and service stays up until then. For today the port is reset at call time: the response does not wait for the switch, and the reset, the subscription.deprovisioned webhook and the customer’s e-mail follow within seconds. If the reset fails, the hourly pass retries it. A line that was never installed (PENDING or PENDING_INSTALL when cancelled) is closed without the customer’s “service deactivated” e-mail, and its port is left as it is unless kurnl had already opened it for the install; the subscription.deprovisioned webhook still follows. One change to a line runs at a time. A cancel that arrives while another change to the same line is still running (a second cancel, a plan change, a suspension) answers 409 {"detail": {"code": "action_in_progress", "message": "…"}} and changes nothing; retry in a few minutes. A cancel that arrives after another one finished answers 409 already_cancelled. 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.

Field reference