https://middleware.kurnl.ca/partner. All calls send X-API-Key; the ones
marked verified also require X-Verified-Session (see Authentication).
Every response is a typed, documented schema — the same shapes are rendered in the
interactive Swagger. Internal fields
(Stripe IDs, operator notes, MAC, user_id) are never exposed.
Identify
Search customers
GET /agent/customers/search?q={term}&limit={n}
Matches contact name, email, phone, short account id, invoice address, building/unit and
service location. Returns masked matches only — enough to identify a caller, not enough to
leak PII before verification.
ambiguous is true (match_count > 1), ask the caller for their
service address or unit, append it to q, and search again — service_locations on each
candidate is the discriminator. Never auto-select a match; if it stays ambiguous, ask
another distinguishing detail or escalate.
Get a customer
GET /agent/customers/{customerId}
Full profile — contacts, invoice address, and your provider’s service locations and
subscriptions. Requires a verified session (verify the caller first). Returns 404 if the
customer is not owned by your provider (no cross-provider enumeration), and for a customer whose
personal data your provider erased on their request. Internal fields are never
returned. For a customer another provider also serves, only your provider’s lines appear.
Verify
See Authentication forPOST /agent/verify/issue and
POST /agent/verify/check. verify/issue accepts channel: "email" (default) or "sms"
(SMS is delivered via Twilio and is inert until configured — returns 503
dependency_unconfigured otherwise).
Answer
Billing summary
GET /agent/customers/{customerId}/billing-summary
Invoices and AutoPay cover your provider’s subscriptions only. payment_method_on_file means a
card saved in your provider’s Stripe account.
Payment history
GET /agent/customers/{customerId}/payments
The customer’s payments. (kurnl has no separate payments table — a paid invoice with its Stripe
payment-intent is the payment record — so this is derived from paid invoices.)
Network health
GET /agent/customers/{customerId}/network-health
Read-only provisioning/activation status of each of your provider’s subscriptions, plus modem
(CPE) status from the ACS when available. No switch control, no live-switch queries.
Communication history
GET /agent/customers/{customerId}/tickets
The customer’s support tickets with your provider — in your groups, or escalated by you to kurnl —
read by their email on file. Another provider’s tickets for the same person never appear.
priority is low, normal, high or urgent (support staff may raise a ticket to urgent; the create takes
the first three). state is the ticket’s state name, compared as written: the ticketing system’s new, open,
pending reminder, pending close, merged and closed, and kurnl’s In Progress, Waiting on Customer,
Waiting on Third Party, Scheduled and Resolved.
Act
Preview a plan change
POST /agent/customers/{customerId}/plan-change/preview
Returns the exact billing impact — recurring change plus the one-time prorated adjustment
for the rest of this calendar month — without applying anything. Use it to explain the
cost before confirming.
Breaking change (September 2026):
one_time_adjustment may now come back null (see
the three cases below); its type value that used to read charge now reads
next_invoice_line. proration_factor (on the response) and due_in_days (inside
one_time_adjustment) are deprecated and will be removed in the next release —
read days / days_in_period instead (see below).one_time_adjustment is nullable. It comes back null, with an explanatory note
and a normal 200, in three cases:- the current calendar month has not been invoiced yet — the monthly invoice will simply
reflect the new price, so there is nothing to charge or credit today (
note: “This month has not been invoiced yet; the monthly invoice will reflect the new price.”) - an invoice exists for the month but its billing-document row is missing (
note: “invoice exists, billing document missing — reconcile”) - the billing-document row exists but the invoice record is missing (
note: “document exists, invoice record missing — reconcile”)
null, one_time_adjustment.type is one of:next_invoice_line— an upgrade. Nothing is charged today; the amount appears as a labelled line on the customer’s next monthly invoice.credit— a downgrade. The amount is credited now and applied to the next invoice.none— the correction rounds to less than $0.50 and is skipped rather than issued;amountis0.00. It isn’t lost — it’s folded into whichever correction runs next.
days / days_in_period replace the old proration_factor: the billing period is the
calendar month, so the adjustment is price difference x days / days in that month. There
is no due_in_days any more — an upgrade is never billed on its own; it always rides the
next monthly invoice. A second plan change on the same calendar day is refused with
409 detail.code = "change_limit_reached" — a change day is billed at the new price, so
the next change is possible from tomorrow; only a kurnl platform admin can override that, and
the AI cannot. While another change to the same line is still running, the answer is 409
action_in_progress: nothing changed, try again later.Apply a plan change
POST /agent/customers/{customerId}/plan-change
Applies the change. Requires confirm: true — call it only after the customer has seen
the preview and agreed. Proration is handled automatically.
Plan-change apply is live wherever your key has access — there is no sandbox gate: a
call with
confirm: true applies a real, billing-affecting change in production. Use it
deliberately. Planned-but-not-shipped hardening: a signed quote_token (preview→apply),
an Idempotency-Key, and a change-confirmation/rollback flow (all planned, none shipped). Validate the read +
verify + escalate loop first.confirm: true the call returns 400 confirmation_required.
Update contact fields
PATCH /agent/customers/{customerId}/contact
Update the account holder’s contact (contract) or the billing contact (invoice). The change
goes through the same rules as an edit in the dashboard and applies immediately — there is no
confirmation email or rollback yet (see Change confirmation & rollback).
contact_type:contract(default) |invoice. Only the fields you send are changed; empty fields are ignored.contract— name, phone and company. The holder’s e-mail is their sign-in address and is refused with422email_is_sign_in_address; the provider changes it in the dashboard. Where the invoices go to the holder, the billing contact follows the change. The change is recorded in the audit log under your provider.invoice— the billing contact. If the invoices still go to the account holder, the change creates a separate billing contact and leaves the holder untouched. Every change is synced to billing; a changed billing e-mail also notifies the old address, not the new one. Refusals:email_required,email_invalid,name_required,same_as_primary(the details are the holder’s own — the invoices already go to them), all422.
Password reset / resend activation
POST /agent/customers/{customerId}/password-reset
Emails the customer a self-service portal link — a password-reset link (15-min token) or a
re-sent account set-up link (14-day token; the provider’s “Account setup email” template).
The customer completes it themselves. The email comes from your provider and the link opens your
customer portal, also for a customer another provider serves.
action:password_reset(default) |resend_activation.resend_activationonly applies while the customer has not yet chosen a password; afterwards it answers 409already_activated— send apassword_resetinstead.
Resend an invoice
POST /agent/customers/{customerId}/invoices/{invoiceId}/resend
Emails a copy of the invoice to the customer: your provider’s own invoice e-mail, with the PDF when that
template attaches it. It goes to the customer’s invoice contact.
- 409
no_invoice_template: your provider has no active invoice e-mail template. - 422
no_address: the customer has no e-mail address to send to; 422 also when the invoice has no billing document yet. - 502
not_sent: the e-mail could not be sent; try again later.
Add a support note
POST /agent/customers/{customerId}/notes
Append an internal note to the customer’s record, attributed to the AI agent. Your staff see it on the customer’s Activity tab.
Never shown to the customer.
Escalation
Create a ticket
POST /agent/tickets
Opens an enriched support ticket routed to the provider’s support group. Not
verification-gated, so it works even when the caller can’t be verified — the primary escalation
path on repeated auth failure or any downstream error. Provide customer_id (must be your
provider’s) or a contact_email.
priority:low|normal|high.ai_summaryandai_diagnosisgo into the customer-facing article;transcriptis stored as an internal note.
Change confirmation & rollback
Rate limits
Exceeding a limit returns
429 (standard rate-limit response; not the error envelope below).
Back off and retry.
Errors
Most/agent errors return a normalized envelope with a stable, machine-readable code
and a request_id you can quote to support — so your agent branches on the code, not the message:
Recommended default on downstream (502/503) and server (500) errors: graceful handover —
create an escalation ticket and offer a human callback rather than retrying. Back off on
429, re-verify on session errors. These behaviours are configurable to your agent design.