See the full guides: CKO-03 Direct Delivery and CKO-03 Home-Drop Delivery.

Endpoint

Authentication

X-Webhook-Secret header — your pre-shared webhook secret.

Routing — session path (session_id present)

With a session_id, delivery_mode is ignored. kurnl resolves the session’s building_id to the building’s line type (operator-side configuration), checks whether the suite’s router is already online, and routes: A suite’s router counts as online when it answered within the last 15 minutes. The check runs when the order arrives; when kurnl cannot read the router’s state, the order takes the install path. GET /serviceability/units shows the same state per suite as cpe_ready before the shopper chooses. A first-line building is reached two ways. From the captive portal the port is known (location_hash) and kurnl provisions it immediately. From address search or a QR landing there is no port — only a building and a suite — so kurnl provisions the suite’s own port (409 suite_not_wired when none is wired), or, for a suite with several connections, holds the order for the operator (below). kurnl fails closed — a payload carrying neither a port nor a unit_id is rejected with 422 (see error codes). A switch port is provisioned without a visit only on an explicit first-line building, or for a second- or third-line suite whose router is already online; outside first line a port alone is never enough. The same rule now applies to the no-session payload: delivery_mode says what you have, the building’s line type decides the outcome.

Shared suites

A suite can have several connections (200-1, 200-2 behind postal suite 200). The session’s units.postal_unit is the unit’s own stored label and does not tell you this — read unit_confirmation_pending in the response. When the session’s unit_id is such a suite, kurnl re-anchors the order to the suite’s first free connection and holds it until the kurnl operator confirms which connection is the customer’s:
On a first-line building route stays provision, but job_id is null, the subscription is PENDING, and no subscription.activated webhook fires. Provisioning and the subscription.activated webhook happen when the operator confirms the connection. On the install route the response is the install-job shape below plus unit_confirmation_pending: true. A retry with the same session_id returns the existing order. Any other repeat of the same subscriber and plan on the suite returns the existing order on the provision (first-line) route, and is refused with 409 Unit already has a pending or active subscription on the install route — it never takes a second connection. When every connection of the suite is taken the call is refused with 409 no_free_connection. When the subscriber arrived via a kurnl Marketplace redirect, you have a session_id (from the ?session_id= query parameter). The session is the source of truth for the provider, plan, location/unit, building, and MAC address — you only supply the subscriber PII:
The discount the session itself carries (discount_code / operator_discount on the session, entered on kurnl before the hand-over) is applied at completion: kurnl records it and lowers its wholesale price for the promo months. You are trusted to pass the discount on to the customer. That code is resolved as of the day the session was created, so one that was valid at the hand-over is honoured even if its dates ended in between. If it was archived meanwhile, it lapses silently: the order still goes through, at the campaign or list price. A discount_code you send in the body is different. On the session form it is the code the shopper typed on your page; on the no-session form it is a code you pass directly. On the session form kurnl checks it as of the hand-over day against the session’s building and honours it if it is a kurnl operator code from the session’s operator_offers (even if its dates ended after the hand-over) or one of your own discount codes, as on the no-session form. Anything else — a typo, another building’s code, or a discount withdrawn in between — is a 400 (Invalid or expired discount code) and nothing is created. Sending back the session’s own code (for example the discount_code from the hand-over URL) is the same as sending none. Check the code against operator_offers or your own codes before you charge; on a 400, retry the completion without the code and the order goes through at the plan’s campaign price if a campaign is live, otherwise at the list price. A bulk plan takes no discount code (422 bulk_discount). If the code cannot be checked right now, the call answers 503 (Could not check the discount code; try again.): retry. If both the session and your payload carry a location_hash or mac_address and they disagree, the session value wins (the conflict is logged). Idempotent: a second call with the same session_id returns the existing subscription without re-provisioning — response shape {subscription_id, subscriber_id, message, unit_confirmation_pending} (no job_id), where subscriber_id is the real kurnl subscriber UUID.

Request body — explicit fields (legacy, no session)

For flows that do not go through a Marketplace session, pass the provisioning fields yourself. delivery_mode states which fields you are sending; the building’s line type (and, outside first line, whether the suite’s router is already online) still decides whether the order provisions a port or raises an install job, so a mismatch is refused with 422:
Building a storefront that owns its funnel? Prefer POST /provisioning/provider-initiated with your X-API-Key — it is the documented CKO-04 path, takes the same delivery_mode / unit_id pair, and tells you which route was taken.
This flow is idempotent per (subscriber email, plan version, location): a retry returns the existing subscription with "Subscription already exists (idempotent replay)" or "Provisioning already in progress" — job_id may be empty on replay.

Response — 200 OK (provisioned — first-line building, or a suite whose router is already online)

Webhook: subscription.activated, then provisioning.completed / provisioning.failed.

Response — 200 OK (install job — the suite needs a visit or a shipped router)

unit_confirmation_pending is true when the unit is a shared suite whose connection the operator still has to confirm; false otherwise. No job_id; subscription status is PENDING_INSTALL. Webhook: subscription.pending_install (not subscription.activated).
The response shape varies with the routing (there is no fixed response model) — always parse job_id as optional and branch on its presence.

Error codes