These endpoints are called on behalf of a subscriber, not by the ISP. They require a subscriber JWT obtained through your authentication flow, not your provider API key.

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:
Each subscription carries billing_day and, next to it, an integer billing_day_max: the latest billing day the provider allows (default 10). A cancelled line carries last_service_day (YYYY-MM-DD, the last day it runs, in the provider’s time zone) and ended_immediately (true when it was switched off on the day of the cancel — from this portal only a line that never went live; last_service_day is then the day of the cancel). Both are null / false without an ended_at. deprovisioned_at stays null until the line is switched off (for a line that never went live: closed, its port reset only if kurnl had provisioned it): a cancelled line with no deprovisioned_at is still in its notice window. Each subscription carries only what the customer sees: its status and dates, the prices the customer pays, the address, the unit and the provider’s public contacts. No network, wholesale or billing-system identifiers.
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 through cancellation.last_service_day, the last day in the provider’s time zone; ended_at is the exclusive midnight after it), 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 — a first-line suite, or a second- or third-line suite whose router is still online), technician (a visit is booked), activate (a never-installed order is switched on now), 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 gross of the invoice the action creates today: tax included, before any credit on the account (credit is applied when the invoice is issued, so the card may be charged less). For a reactivation it is the true-up of the rest of the current calendar period from today; 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, or a CPE-ready second- or third-line suite), where billing_starts is now and first_charge is the first invoice: the setup fee plus the partial month from today. first_charge is null when the figure cannot be computed right now. monthly_price is the line’s own price before tax, never the catalogue’s. has_payment_method is whether the customer’s Stripe customer has a default card with the provider — the card every charge uses.

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. The body is optional. Send {"expected_path": "remote" | "technician" | "activate" | "none"} — the path of the preview you showed (a withdrawal is none) — to guard against the route having changed in between: when the path kurnl now evaluates is a different one, the call answers 409 path_changed and nothing is written. Any other expected_path value answers 422 (request validation) and nothing is written. Without a body the call runs whatever path it evaluates, as before. Reload the preview and confirm again after a path_changed. 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.
case: "reactivate", path: "remote" — a first-line suite whose port still resolves, or a second- or third-line suite whose router is online (it answered within the last 15 minutes and the suite has a single connection). 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, the invoice’s gross as Invoice Ninja totals it — tax included, before any credit on the account — 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 whose router is not online, 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.
A subscription may therefore carry more than one install job — its completed first install and the reinstall. At most one is open at a time; 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), or on a second- or third-line suite whose router is online: the first order finally happens now, with no visit. The first and recurring invoices are created for the row — the first one covers today to the end of the month, whatever day the cancelled order was placed — 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; a charge Stripe did not confirm answers 409 payment_unconfirmed and keeps the documents, so a retry charges the same invoice), 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 whose suite’s router is not online (with the router online the activate path above applies instead). 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.
When the eligibility says no, the answer is 409 with the reasons in the customer’s words:
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.
  • card_declined, no_payment_method — the activate path’s charge: declined (the documents are deleted) or no card on file.
  • payment_unconfirmed — Stripe gave no clear answer; the provider checks the payment. Do not retry at once.
  • activation_incomplete — the activation was paid but the line could not be finished; the provider finishes it.
  • reactivation_incomplete — the port is on but the line could not be written; the provider finishes it.
  • reactivation_in_progress — another request is bringing the same line back right now (a double submit, or the dashboard at the same time); nothing was done. Try again in a few minutes.
  • action_in_progress — another change to the same line is still running (a cancellation, a plan change, a suspension); nothing was done. Try again in a few minutes.

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.
When the invoices still go to the account holder, this splits a separate billing contact off. Fields you leave out start from the holder’s details, and the holder’s own details are never changed. A separate billing contact must have an e-mail address and a name or a company, and it must be a different person from the holder. Response (200): { "success": true, "contact": { …the billing contact… }, "state": "separate" } Errors, as {"detail": {"code", "message"}}: When the billing e-mail address changes, the old address receives an information mail; the new address does not. 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, and invoice_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, the old address is 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 no email, 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.
Response: {"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

Response: {"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 by invoice_date descending:
Returns 404 "No subscriber record found for this account" when the account has no subscriber record (same for the other /me/* sub-resources).

POST /partner/subscriber/me/invoices//pay

Pays one failed invoice (BOUNCED, OVERDUE_30, OVERDUE_90) now with the card on file — the holder only. The charge uses the same idempotency key as the next automatic retry, so the two never charge twice.
Refusals are 409 {code, message}: already_paid, not_payable (an open invoice is collected automatically), no_invoice_document, no_payment_method, payments_not_configured, and payment_unconfirmed — Stripe gave no clear answer, the invoice stays open and the provider checks the payment (do not retry at once). A declined card answers 402 {"code": "charge_failed", "message": "Charge failed: …"}. An invoice that is not the account’s answers 404.

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 billing system (Invoice Ninja) of each provider the customer is linked to, 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. service_provider_id and provider_name name the provider that issued the note: a customer who is, or was, served by two providers gets both providers’ notes in one list. The PDF of a credit note is served by GET /api/v1/credit-notes/{id}/pdf?service_provider_id={service_provider_id} with the subscriber’s session — pass the row’s service_provider_id, since credit ids repeat across providers’ billing systems. A note that does not belong to the subscriber, or a provider the subscriber is not linked to, answers 404. Returns 404 when the account has no subscriber record and 502 when any of the billing systems is unreachable (never an empty or partial list in that case).

GET /partner/subscriber/me/usage

Per-month download/upload usage in GB for one subscription, oldest month first: the primary line, or the line ?subscription_id= names (any of the caller’s own lines, in any status; an id that is not the caller’s answers 404 Subscription not found). Months are calendar months in the provider’s time zone (the one set on the ISP profile): a month runs from local midnight to local midnight, and month is the local month. Every month from the one the subscription started in to the current month to date, at most 60 months. A line that has ended or been switched off stops at the earlier of its ended_at and deprovisioned_at: later months are not listed and its last month counts only up to that moment, so a cancelled line does not show the traffic on its port afterwards. The last month listed is the month of the line’s last service day: a line that ends at midnight on 1 November lists October as its last month. Without a start date, the last 6 complete calendar months in the provider’s time zone 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 (only without subscription_id).

GET /partner/subscriber/status-history

The subscriber’s own connection timeline, newest first, at most 50 events: {"events": [{"id", "subscription_id", "started_at", "resolved_at", "ongoing"}]}. subscription_id (additive) is the line the event belongs to; it is null on older events that were recorded without one. Without a parameter the timeline lists every line’s events. ?subscription_id= limits it to that line’s events (events without a subscription_id are left out); an id that is not one of the caller’s own subscriptions answers 404 Subscription not found. Without the parameter it degrades to an empty list on any failure so it stays loadable during outages.

GET / PATCH /partner/subscriber/me/preferences

The signed-in person’s own communication preferences: one channel per topic. GET returns them with the defaults applied; PATCH takes any subset of the channel keys and answers the same shape.
marketing_email, outage_notifications and sms_transactional are derived from the channels and read-only in effect; they are still accepted in a PATCH for older integrations (marketing_email: false → marketing_channel: off, sms_transactional: true → texts added to every topic that is not off). preferred_channel (email | phone) is informational for the provider’s staff. paper_billing is the account’s and only the holder’s payload carries it; it cannot be set here. updated_at is null until the first change. sms_available says whether the provider can text at all (its Twilio settings); has_phone whether this person’s own contact carries a readable phone number. A PATCH that adds texts — sms or both on a topic that had none, or sms_transactional: true — answers 409 {"code": "sms_not_available"} without the former and 409 {"code": "no_phone"} without the latter; re-sending an unchanged text channel needs neither. A value a topic does not offer (for example marketing_channel: "sms") answers 422.

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 31, up to the provider’s own maximum (the billing_day_max on each subscription of GET /partner/subscriber/me, default 10); the default day is 1. Days after the 28th are charged on the last day of shorter months.
confirm_invoice_now is optional (default false); see the 409 invoice_now_required case below.
  • effective_period is the first calendar month the new day applies to, as its 1st.
  • invoiced_now is true when the request was sent with confirm_invoice_now: true and this month’s invoice was issued as part of it.
  • next_send_date is when the recurring invoice will next send, or null when 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 — the one exception is the confirmed invoice below, which is the month’s regular invoice issued early. If the current month has already been invoiced, the new day takes effect from the next month instead.
The subscription’s current day is always accepted, even above the provider’s maximum: a provider that lowers its maximum keeps its customers’ existing days. Status codes: 200 on success · 404 when the subscription is not the caller’s · 422 when billing_day is outside 1 to the provider’s maximum ("Choose a billing day from 1 to N.") · 409 invoice_now_required when the current month has not been invoiced yet and the chosen day has already passed (or is today) · 502 when the day was saved but the next invoice could not be rescheduled (retry). Two other 409s carry a plain string detail: a past day on a line that is neither active nor pending ("This month's invoice has not been issued yet and that day has already passed. Change it after this month's invoice is issued."), and a concurrent confirm ("This month's invoice is being issued right now. Please try again in a minute.").

The invoice_now_required 409

Choosing a day that has already passed in a month that is not invoiced yet would mean billing that month now. kurnl refuses with a quote instead of doing it silently:
amount is what the period invoice would be, charged to the saved card when auto_pay is true and otherwise due for payment. Show it to the customer; if they agree, send the same request again with "confirm_invoice_now": true — the invoice is issued, invoiced_now is true, and billing_day applies from next_period. Alternatively pick a day still ahead of today.

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 has no suites — the portal then shows the marketplace link. The building’s route is the default for a suite whose router is not online: install (second line, third line or unclassified — a technician or a shipped router) or provision (a first-line building). Each unit carries its own route and cpe_ready; order by the unit’s route. cpe_ready is true when the suite’s router is already online (it answered within the last 15 minutes) and the suite has a single connection. A unit’s route is provision on a first-line building, and on a second- or third-line building when cpe_ready is true; otherwise install (an unclassified building installs even a ready suite). On the provision route 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. On a building the provider does not serve every unit carries cpe_ready: false and the building’s route. A suite is available when it is free and no live order references it. running_lines (additive) lists every running line (status ACTIVE, SUSPENDED or PAYMENT_FAILED) a mover may end, on a day they pick (end_current_on): ACTIVE first, then the newest start, then by id, so the order is the same on every call. ends_at is the line’s end day when one is set, else null. A customer with several lines picks one of them, or none. current_subscription is kept and is the first of running_lines (null when there is none). 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. The plans are the ones offered to this customer — existing when they have a running line with the provider, otherwise new — so a plan for existing customers only is not listed. GET /partner/plans/versions applies the same rule; its for_existing_subscribers parameter is accepted and ignored.

GET /partner/subscriber/me/orders/preview?building_id=&plan_version_id=&unit_id=&requested_start_date=&end_current_subscription_id=&end_current_on=

A dry run of the order’s preparation: what the order core would refuse (a held suite, an unknown plan, the order window) comes back as a 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. promo_setup_fee is the campaign’s setup fee when it overrides the version’s (else null) and promo_ends_after_months the promotion’s duration in months (null = for as long as the line runs). The promo is billed from activation, for that many full calendar months (the first, partial month too), then the plan’s price. route is the suite’s route at the time of the preview, from a fresh read: a second- or third-line suite whose router is online answers provision, billing_starts: now and a first_charge: the gross of the first invoice the order creates today — the setup fee plus the partial month from today at the promo prices, tax included, before any credit on the account (null when it cannot be computed right now). Send this route with the order. ends_current appears with end_current_subscription_id. ends_at is the exclusive end day for the end_current_on you sent (null while none is given; today gives tomorrow, since the line runs through today), last_service_day the last day that line runs and ends_immediately whether it is switched off at once (never for a running line). A missing or past end_current_on is a blockers sentence, like the order’s 422s.

POST /partner/subscriber/me/orders

route (optional) is the route the customer reviewed — send the preview’s route. kurnl decides the route again with a fresh read when the order arrives; when the suite’s router has gone offline or come online in between, the order would be charged differently from what the customer saw, so it is refused with 409 route_changed and nothing is written (the running line is not ended either). Run the preview again and show the new review. Without route the order is placed on the fresh route. Any other value is a 422 validation error. Runs the same order core as the marketplace checkout with payment: saved_method (no hosted session) and source: portal. On the install route the card on file is charged once the install is done, and the row lands PENDING_INSTALL with its install job, the provider’s install ticket and — on a third-line building — the booking invitation. On the provision route the card is charged at once (see the options above). With end_current_subscription_id the running line is cancelled for end_current_on after the order stands (reason moving) — not for the move-in date, which stays optional and does not set it; a failed order ends nothing. end_current_on is required with end_current_subscription_id, a calendar day, earliest today in the old line’s provider’s time zone. Today keeps the old line on to the end of today: it is cancelled for the next day, is switched off after local midnight, and today is not refunded. Any other day follows the cancel rule: the line runs through the day before it. ended_subscription.ends_at is that exclusive end day. Audit subscription.ordered_from_portal.
409 {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 one of this account’s running lines — any of them may be named), no_provider, route_changed (the suite’s route changed since the preview: “This suite now needs a technician visit — review your order again.” or “This suite can now start today without a visit — review your order again.”). The core’s own refusals keep their status: 404 plan or suite, 409 held suite, 422 order window. 422 (a sentence, checked before anything is created, so a refused order creates nothing): Pick the day your current line ends. when end_current_subscription_id comes without end_current_on; The earliest cancellation date is today, October 14, 2026. when end_current_on lies before today in the provider’s time zone (the sentence names that day).