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:
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.
Request body — session-based (recommended)
When the subscriber arrived via a kurnl Marketplace redirect, you have asession_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:
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."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)
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).