Endpoint
X-API-KeyRate limit: 120 requests / minute
When to use
PAY-03 is for ISP-initiated provisioning — you have the subscriber’s details (from your own storefront, a sales CRM, a signed contract, or a migration from another system) and want to create their account without redirecting them through a kurnl checkout flow. You can name the premise two ways:Request body
delivery_mode: "direct" (the default) provide at least one of location_hash or location_identifier (both is legal; location_hash wins). With delivery_mode: "home_drop" provide unit_id instead; location fields are ignored. There are no top-level subscriber.firstname / lastname / phonenumber fields — names and phone numbers live in invoice_contact_detail / contract_contact_detail. subscriber.password is optional; when set it controls the subscriber’s self-service login.
discount_code is optional: a provider code of yours or a kurnl operator discount code (see
Operator discounts). A code that does not resolve is a 400
(Invalid or expired discount code) and nothing is created. A provider code of yours changes nothing kurnl bills on a partner-billed line; a kurnl operator discount lowers the wholesale price kurnl invoices you.
billing is optional. Without it, or with owner: "partner", kurnl provisions and you bill — no Stripe records, no invoices. With owner: "kurnl" you name the customer in your Stripe account whose card your storefront saved; kurnl verifies it can bill (see the 409/422 rows below), adopts the customer and its default payment method, and bills the subscription like a marketplace order — a prorated first invoice charged at activation, then monthly on billing_day. owner: "kurnl" without stripe_customer_id is a 422 model error. Full contract: Who bills.
Response — 200 OK
Your confirmation screen should differ between the two:
provision means the
customer is coming online now, install means someone will contact them to book
a visit.
Webhooks: subscription.activated fires immediately at subscription creation — before the port is live. provisioning.completed (or provisioning.failed) fires after the background job finishes. (A shared-suite order is the exception — see below.)
Shared suites
A suite can have several connections (GET /serviceability/units returns it as one entry with a postal_unit). An order on such a suite (delivery_mode: "home_drop" with its unit_id) is re-anchored to the suite’s first free connection and held until the kurnl operator confirms which connection really serves the customer. The response depends on the route the building takes.
On a first-line building, where the order would otherwise be provisioned at once (route: "provision"), the held response is:
routestaysprovision, butjob_idisnulland the subscription isPENDING: nothing is provisioned and nosubscription.activatedwebhook fires yet.- When the operator confirms the connection, kurnl activates the subscription and provisions the port; then
subscription.activated, followed byprovisioning.completed/provisioning.failed, fire as for any other order. - On the
installroute the response is the usual install response (route: "install",job_id: null,PENDING_INSTALL) withunit_confirmation_pending: true; the operator’s confirmation picks the connection the technician installs on. - A retry never takes another connection. The check is per (subscriber, plan version, suite): on the
provisionroute a retry returns the existing order (see Idempotency); on theinstallroute it is refused with409Unit already has a pending or active subscription. - A suite with a single connection is unaffected and answers
unit_confirmation_pending: false.
Idempotency
Idempotency is scoped to the exact (subscriber, plan version, location) triple — the same email + plan at a different location provisions normally (a genuine second line), also when the customer already has a line with you or with another provider.SUSPENDED subscriptions count as live for this check.
Any other live order on the same port is refused with 409 port_in_use: another subscriber’s, or another plan version of the same subscriber. A line in PENDING, PENDING_INSTALL, ACTIVE, SUSPENDED or PAYMENT_FAILED holds its port. A line in PAYMENT_FAILED does not count for the replay, so a retry of its order answers port_in_use too.
For a shared suite the key is the suite, not the connection: the same subscriber + plan on any connection of the suite is a replay. A replay of a held order answers "Subscription already exists (idempotent replay)" with unit_confirmation_pending: true while the operator has not confirmed, and job_id null (or the latest job id once provisioning ran).
A replayed request returns 200 with the existing subscription, but only while its discount_code still resolves: the code is resolved before the idempotency check.
message is "Provisioning already in progress" when a provisioning job is still pending/running, or "Subscription already exists (idempotent replay)" otherwise. job_id may be the empty string when no job record exists. Safe to retry on network errors. A replay that names a different billing.stripe_customer_id for the same subscriber is refused with 409 stripe_customer_in_use rather than rebinding the subscriber.