Authentication
Authorization: Bearer <subscriber_jwt> — the JWT issued to the subscriber after login.
Account holder and authorized users
An account has one account holder (the person who signed up and is named on the agreement) and up to five authorized users, each with their own login. Both see the same account; every endpoint here works for both unless it is marked holder only. A holder-only endpoint answers 403 with{"detail": {"code": "holder_only", "message": "Only the account holder can do this."}} to an authorized user.
Holder only: changing the invoice contact, adding or removing authorized users, cancelling, and everything that
pays — payment method, AutoPay, billing day, paying an invoice, the paper-billing request tickets — plus a PIPEDA
deletion request. Preferences, the second factor and /me/contact are the signed-in person’s own.
Endpoints
GET /partner/subscriber/me
Returns the subscriber record, all their subscriptions, any anonymous external-checkout sessions linked to the account, and provider branding:An account with no subscriber record yet (freshly registered, nothing ordered) gets a 200 with
"subscriber": null and empty subscriptions — not a 404.GET /partner/subscriber/me/reactivation
The account’s most recent cancelled subscription and whether it can be brought back — read-only, for every account user.case is withdraw while the cancellation has not taken effect yet (the line still runs until
cancellation.ended_at — midnight in the provider’s time zone), reactivate once the line was switched off, resume when the line was never live (the
order was cancelled before its install), null when nothing is cancelled. eligible
is false whenever blockers names a reason (unpaid invoices, the suite served by someone else since, a line
billed by the provider directly, …). path says what a reactivation takes: remote (the port comes back within
minutes), technician (a visit is booked), none for a withdrawal. holder_only is true for an authorized
user — the actions themselves are the account holder’s.
billing.first_charge is the prorated rest of the current calendar period from today (net, before tax) for a
reactivation, null for a withdrawal and for a resume; billing_starts is now for a reactivation and
activation for a resume (nothing is charged until the line is on) — except on the activate path (planning 152:
a never-installed line on a first-line building), where billing_starts is now and first_charge is the
first invoice’s estimate (setup fee plus the first month); monthly_price is the line’s own price, never the
catalogue’s.
POST /partner/subscriber/me/subscriptions//reactivation
The action behind the preview, for the account holder only (holder_only 403 for an authorized user). The
preview’s case decides what it does.
case: "withdraw" — takes the pending cancellation back: the line returns to the status it had before the
cancellation (status), its billing continues, any refund credit that was still open is voided and days the
cancellation took back are invoiced again (billing summarises what was done).
case: "reactivate", path: "remote" — brings an ended line back on the same row: the switch port is
provisioned first (VLAN, bandwidth, the device’s MAC), and only on a confirmed write the line goes ACTIVE
again on its suite, its recurring billing resumes and the rest of the current period from today is invoiced
(first_charge, net before tax, charged to the payment method on file through the usual invoice path). The
call waits for the port — allow about a minute.
case: "reactivate", path: "technician" — a second- or third-line suite, or a first-line suite whose port
no longer resolves: somebody has to go. The call schedules the visit rather than reactivating: the line goes
back to PENDING_INSTALL on its own plan, price and payment method, its suite is held again, and a second
install job of kind: "reinstall" is opened and booked the way a fresh order is — booking: "customer" on a
first- or third-line building (the customer receives the booking e-mail), "operator" on a second-line one
(the dispatch is scheduled by the operator). Nothing is charged now. The line goes ACTIVE — and its
recurring billing resumes, with the rest of that month invoiced from the visit day — once the technician has
been (the CPE’s first configuration pull, or the completed dispatch). The customer gets the reactivation
e-mail at that point.
install_jobs.kind is install or reinstall.
case: "resume", path: "activate" — the order was cancelled before its install on a first-line building
whose suite’s port resolves (planning 152): the first order finally happens now. The first and recurring invoices
are created for the row, the balance is charged to the card on file (a decline answers 409 card_declined with
the documents deleted and nothing else touched; no card → 409 no_payment_method), the port is provisioned
without the reactivation port guard, the line goes ACTIVE on its suite and the welcome mail goes out. A port that
will not switch on after the money moved leaves the line active and answers provisioned: false (the provider has
the ticket). A first-line suite whose port does not resolve is refused: “This suite is not wired yet — contact your
provider.”
case: "resume" — the order was cancelled before its install (by the customer, the operator, or the
stale-install clock — cancellation_reason: "install_timeout") on a second- or third-line building. The call
picks the order up again on the same row, plan, price and payment method: the line goes back to
PENDING_INSTALL, its suite is held again, a fresh install job of kind: "install" is opened and handed to
the fresh-order tail — booking: "customer" on a third-line building (the booking e-mail goes out),
"operator" on a second-line one (the router is shipped from the install queue) — and the provider’s install
ticket is raised as at checkout. Nothing is charged now. The line goes ACTIVE and is billed as a first
order once the install is done. A never-installed order on a first-line building is not resumable
(eligible: false, “never activated — ask your provider”); one that ended through a provider switch is not
either. requested_start_date is kept while it is still ahead, otherwise reset to today.
port_in_use (another device is on the port) and provisioning_failed (the switch write did not confirm) also
raise an operator ticket; nothing is changed on the line in either case. install_unavailable means the
reinstall job could not be opened — the line is left as it was.
GET /partner/subscriber/me/details
Returns the full account profile used by the Account page.customer_number is the provider-scoped, human-readable customer number (seven random digits with a dash after the fourth, e.g. 0482-117 — leading zeros are part of the number, so treat it as a string; also printed on invoices); it is null only while no number has been allocated yet:
contract_contact is the account holder — the person named on the agreement, whom the provider names the customer.
invoice_contact is the billing contact: the same person unless a separate billing contact was set (by the provider,
or by the holder through PATCH /me/invoice-contact), which billing_contact_differs reports (compared by name,
e-mail, phone and company, not by row). user.contact is the
signed-in person (the holder’s contract contact, or the authorized user’s own record). A user with no subscriber
record gets a 404 (unlike /me, which returns "subscriber": null).
PATCH /partner/subscriber/me/invoice-contact
Holder only. Sets a separate billing contact, the person the invoices and billing messages go to. All fields are optional strings: a field you leave out keeps its current value, an empty string clears it.{ "success": true, "contact": { …the billing contact… }, "state": "separate" }
Errors, as {"detail": {"code", "message"}}:
When the billing e-mail address changes, the old and the new address each receive an information mail. After the
change, kurnl also updates the billing contact on the provider’s Invoice Ninja client, if there is one. If that
update fails, the change still stands and the response is still 200.
POST /partner/subscriber/me/invoice-contact/use-holder
Holder only. Sends the invoices to the account holder again. The separate billing contact is removed, andinvoice_contact is then the holder. No request body.
Response (200): { "success": true, "contact": { …the holder… }, "state": "shared" }. A 409 not_separate
means the invoices already go to the holder. When the e-mail address changes, both addresses are told, as with the
PATCH.
PATCH /partner/subscriber/me/contact
The signed-in person’s own details — for the holder the contract contact, for an authorized user their own record. All fields optional; there is noemail, because the e-mail is the login itself. The holder’s edit also updates the
billing contact while it describes the same person; a billing contact the provider split off is left alone, so a
customer can never create a split from the portal.
{"success": true, "contact": { "firstname", "lastname", "email", "phonenumber", "company" }}.
Authorized users
GET /partner/subscriber/me/authorized-users:
POST /partner/subscriber/me/authorized-users (holder only) with {"firstname", "lastname", "email", "phonenumber"?}
creates the person’s login and e-mails them a set-up link valid for 14 days (the provider’s Authorized user
invitation template). Returns 201 with the row plus invitation_sent. 409 with a code of
email_has_portal_account (the e-mail already signs in somewhere) or limit_reached; 422 for a missing or
invalid e-mail.
POST …/{id}/resend-invite (holder only) sends the link again while the person has not set a password; 409
already_activated afterwards. DELETE …/{id} (holder only) removes the login and the person, 204; 404 for
an id that is not one of this account’s authorized users.
PATCH /partner/subscriber/me/password
{"success": true, "message": "Password updated successfully"}. A wrong current password returns 400 "Current password is incorrect".
GET /partner/subscriber/me/invoices
Returns the subscriber’s kurnl invoices (local invoice rows, not Invoice Ninja objects), sorted byinvoice_date descending:
"No subscriber record found for this account" when the account has no subscriber record (same for the other /me/* sub-resources).
GET /partner/subscriber/me/credit-notes
Returns the subscriber’s credit notes — cancellation refunds and credits, plan-change and operator credits — read live from the provider’s billing system (Invoice Ninja), newest first. Drafts, archived and voided notes are never listed. Unlike invoices, nothing is stored on the kurnl side.state is the customer-facing status: open (issued, not yet settled), refunded (refunded to the card), paid_out (paid out by other means), applied (applied against an invoice). amount is the gross amount in the provider’s currency.
The PDF of a credit note is served by GET /api/v1/credit-notes/{id}/pdf with the subscriber’s session — the same access model as invoice PDFs; a note that does not belong to the subscriber answers 404.
Returns 404 when the account has no subscriber record and 502 when the billing system is unreachable (never an empty list in that case).
GET /partner/subscriber/me/usage
Per-month download/upload usage in GB for the subscriber’s active subscription, oldest month first: every month from the one the subscription started in to the current month to date, at most 60 months. Without a start date, the last 6 complete calendar months plus the current month. Each month is{"month": "YYYY-MM", "download_gb": 12.34, "upload_gb": 5.67}. status is "ok" when any month carries traffic; "no_data" when every month reads zero (the months are still listed) or the line has no switch port yet ("months": []); "no_subscription" with "months": [] when the account has no subscription.
GET /partner/subscriber/status-history
The subscriber’s own connection timeline, newest first:{"events": [{"id", "started_at", "resolved_at", "ongoing"}]}. Degrades to an empty list on any failure so it stays loadable during outages.
PATCH /partner/subscriber/me/billing-day
Choose the day of the calendar month one of the subscriber’s own subscriptions is invoiced and charged on. Allowed values are 1 to 10; the default is 1.effective_periodis the first calendar month the new day applies to, as its 1st.next_send_dateis when the recurring invoice will next send, ornullwhen the subscription has no recurring invoice yet.
The billing period is always the calendar month, so this changes only when money is
collected — never what is owed. No invoice, no credit note and no correction is ever
produced by a billing-day change. If the current month has already been invoiced, the new
day takes effect from the next month instead.
200 on success · 404 when the subscription is not the caller’s ·
422 when billing_day is outside 1–10 · 409 when the current month has not been
invoiced yet and the chosen day has already passed (or is today) — pick a day still ahead
of today, or wait until this month’s invoice is issued · 502 when the day was saved but
the next invoice could not be rescheduled (retry).
GET/POST /partner/subscriber/me/payment-method*
GET .../me/payment-method, POST .../me/payment-method/setup and
POST .../me/payment-method/confirm manage the subscriber’s saved card in their
provider’s own Stripe account. If that provider has no Stripe connection, each returns
409:
Subscriber management for ISPs (listing subscribers, looking up accounts, triggering cancel) is done through the dashboard, not the partner API. Subscribers are created automatically as part of the provisioning flows — see External Checkout and Provider-Initiated Provisioning.
Ordering service at another address (planning 151)
A signed-in customer orders a new line on their own account — a mover, or a customer whose last line ended and whose suite is now served by someone else. Three holder-only routes, keyed on the token’s subscriber; the provider is the portal’s (a live line’s provider first, else the newest). The marketplace’s “that e-mail already has an account” does not apply here: the account is known.GET /partner/subscriber/me/orders/options?building_id=
served is false when the provider does not cover the building or the building cannot take an order this way
(unclassified, no suites) — the portal then shows the marketplace link. route is how a fresh order there is served:
install (second/third line — a technician or a shipped router) or provision (a first-line building, planning 152:
the suite’s port is provisioned on payment; the order charges the card on file at once and answers route: "provision", first_charge, booking: null; a declined card → 409 card_declined). A suite is available when it is free and no live order references it. current_subscription
is the running line a mover may end on the move-in date. postal_unit (additive) is the suite label the customer’s postal address uses when it differs from unit_number, the unit id; null when the unit carries no separate label. Show it to the customer in place of unit_number when set; the id stays the key.
GET /partner/subscriber/me/orders/preview?building_id=&plan_version_id=&unit_id=&requested_start_date=&end_current_subscription_id=
blockers sentence, never as an error; a missing card on file is a blocker too. Nothing is
charged on the install route (first_charge null, billing_starts: activation); the promo is the plan version’s
live campaign.
POST /partner/subscriber/me/orders
payment: saved_method (no hosted session — the card on
file is charged once the install is done) and source: portal; the row lands PENDING_INSTALL with its install
job, the provider’s install ticket and — on a third-line building — the booking invitation. With
end_current_subscription_id the running line is cancelled for the move-in date after the order stands (reason
moving); a failed order ends nothing. Audit subscription.ordered_from_portal.
{code, message}: no_payment_method (add a card first), not_served (another provider’s plan),
suite_required, suite_mismatch, not_running (the line to end is not this account’s running line),
no_provider. The core’s own refusals keep their status: 404 plan or suite, 409 held suite, 422 order window.