Base URL: https://middleware.bso-projects.com/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.
Disambiguation. When 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, service locations, subscriptions. Requires a verified session (verify the caller first). Returns 404 if the customer is not owned by your provider (no cross-provider enumeration). Internal fields are never returned.

Verify

See Authentication for POST /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

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 per-subscription provisioning/activation status 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, read by their email on file.

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”)
The last two mean the invoice and the correction-tracking document disagree with each other — read that as an operator-escalation state, not a number to quote to the caller.When it is not 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; amount is 0.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 — 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.

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.
Without confirm: true the call returns 400 confirmation_required.

Update contact fields

PATCH /agent/customers/{customerId}/contact Update the customer’s approved contract or invoice contact fields. Benign write — no billing or network impact. 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.

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.
  • action: password_reset (default) | resend_activation.
  • resend_activation only applies while the customer has not yet chosen a password; afterwards it answers 409 already_activated — send a password_reset instead.

Resend an invoice

POST /agent/customers/{customerId}/invoices/{invoiceId}/resend Emails a copy of the invoice to the customer.

Add a support note

POST /agent/customers/{customerId}/notes Append an internal, operator-visible note to the customer’s record, attributed to the AI agent. 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_summary and ai_diagnosis go into the customer-facing article; transcript is stored as an internal note.

Change confirmation & rollback

Not yet implemented. Contact/personal-data changes are applied immediately with no confirmation email and no rollback link — do not present this as a safeguard to callers or rely on it as a mitigation against socially-engineered verification. An out-of-band confirmation + rollback flow is planned; until it ships, treat every agent-initiated change as final and escalate doubtful requests to a human instead.

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:
Envelope gaps — handle plain {"detail": "…"} bodies too: the plan-change apply endpoint delegates internally and returns plain HTTPException bodies without a code (400 “Only active subscriptions…”, 400 same-plan, 404 “New plan version not found”, 400 cross-provider); truly unhandled errors return FastAPI’s default 500 {"detail": "Internal Server Error"}; and an account with API access disabled surfaces as 403 with code: invalid_api_key (not agent_access_disabled) because auth errors are re-wrapped. Branch on code when present, fall back to detail + status otherwise.
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.