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, 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. 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:
  1. POST /serviceability/check with the address → a building_id.
  2. GET /serviceability/units?building_id=… → the suites, each with available.
  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 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 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 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). 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.

Field reference