When to use this

Use CKO-03 direct delivery when:
  • The subscriber’s building is classified as first-line in kurnl — the switch port is known (captive-portal flow, location_hash) or kurnl resolves the suite’s own port (address search, QR landing), so kurnl can provision it directly
  • Or the building is second or third line and the suite’s router is already online — kurnl provisions the suite’s port the same way, without a visit
  • You run your own checkout page and want full control over the payment and signup experience
  • Your plan is listed on the kurnl marketplace and you want subscribers to start there
If the building is second or third line and the suite’s router is not online, or the building is unclassified (even when the router is online), the same callback creates an operator install job instead. See CKO-03 home-drop delivery.

How kurnl routes the callback

For POST /partner/external-checkout/complete with a session_id, the delivery_mode field is ignored. kurnl resolves the session’s building_id to the building’s line type — operator-side configuration that partners never set — checks whether the suite’s router is already online, and routes: A router counts as online when it answered within the last 15 minutes. kurnl checks this when the callback arrives, so a suite that was ready when the shopper chose it can still take the install path. GET /serviceability/units shows the state per suite as cpe_ready. kurnl fails closed: the switch 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. When kurnl cannot read the router’s state, the order takes the install path. A session carrying neither a port nor a unit_id is rejected with 422 (see error handling).
Because the line type is operator configuration and the router’s state can change, your integration cannot know in advance which response it will get. Treat job_id as optional: if it is present, provisioning has started; if it is absent, the subscription is pending installation. Handle 422 gracefully by surfacing the error to support rather than retrying.
delivery_mode still controls the routing only for legacy no-session payloads (see the explicit-fields reference below).

How it works

Step-by-step

1. Configure your checkout URL

In the provider dashboard, open Checkout (/checkout-settings) and set the Store checkout URL. When a subscriber selects your plan, the kurnl Marketplace creates a checkout session and redirects them to your page with a single query parameter appended: Example redirect:

2. Read the session

On page load, fetch the session to get the plan and location the subscriber selected:
The embedded plan_versions object gives you the plan name and price to display. Reading the session here also protects it: from your first read on, it no longer expires, so a slow payment cannot cost the order. A session your page never reads expires 2 hours after it was created, and a 410 Gone then means the subscriber must restart from the Marketplace. See Checkout Sessions for the full session API, including the PATCH upsell endpoint for swapping plans mid-checkout.
Legacy handoff: integrations built before the session flow received location_hash, plan_version_id, and service_provider_document_id as individual query parameters. That handoff is obsolete — new integrations should always read the session. The callback still accepts explicit fields for non-Marketplace flows (see the field reference below).

3. Collect payment and subscriber details

Run your normal checkout flow. Collect the subscriber’s:
  • Name, email, phone (optional)
  • Invoice address (the address printed on the invoice)
  • Payment method
kurnl does not receive payment details. You handle payment processing entirely on your side.

4. Call kurnl after successful payment

After payment succeeds, POST to /partner/external-checkout/complete with the session_id and the subscriber’s details. kurnl reads the plan, location, building, and provider from the session — you never re-send them:
Successful response — 200 OK (provisioned directly — first-line building, or a suite whose router is already online):
Successful response — 200 OK (install job — a suite whose router is not online, a shared suite, or an unclassified building):
No job_id in the second shape — provisioning has not started; the subscription is PENDING_INSTALL and kurnl fires subscription.pending_install instead of subscription.activated. See home-drop for what happens next. Store the subscription_id and subscriber_id in your system — you’ll need them for any future subscriber management calls. The callback is idempotent: a second call with the same session_id returns the existing subscription and the real subscriber_id, without job_id, with the message "Subscription already exists for this session_id (idempotent replay)".

Shared suites

A suite with several connections (200-1, 200-2 behind postal suite 200) is re-anchored to its first free connection and held until the kurnl operator confirms which connection is the customer’s. On a first-line building the callback answers 200 with "job_id": null, "route": "provision" and "unit_confirmation_pending": true; the subscription is PENDING and provisioning does not start. Retrying the callback with the same session_id returns the existing order; without one, a repeat returns the existing order on the provision route and 409 Unit already has a pending or active subscription on the install route. When every connection is taken the callback is refused with 409 no_free_connection. Details: POST /external-checkout/complete.

5. Handle webhook events

kurnl fires events to your configured webhook_url at each stage: At provisioning.completed, the subscriber’s internet is live. This is the right moment to send them a confirmation email. See Webhooks for signature verification and retry behaviour.

Field reference

Explicit fields (legacy, non-Marketplace flows only)

Without a session_id, routing is controlled by delivery_mode:

Subscriber fields (required in both flows)

Optional fields

Contact detail fields

Address fields


Error handling

410 body shape — unlike other errors, the detail is a nested object:
({"detail": {"detail": "Session cancelled"}} for cancelled sessions — no expired_at.) On 5xx errors: Safe to retry the full request. kurnl rolls back any partial state automatically on failure. On network timeout: Retry the request — it is idempotent. The same session_id returns the existing subscription. The explicit-fields flow is idempotent too: a retry with the same subscriber email + plan_version_id + location returns the existing subscription with the message "Subscription already exists (idempotent replay)" (or "Provisioning already in progress" while the job is still running; job_id may be empty on replay).

Anonymous variant

If you own the end-user relationship and don’t want kurnl to store subscriber PII, use the anonymous variant instead. You provide an external_subscription_id (your own identifier) instead of a subscriber object. kurnl provisions the port but creates no subscriber record.
With the anonymous variant, subscribers have no kurnl self-service portal access and kurnl cannot issue invoices on your behalf.