Appearance
pbx
8 endpoints.
GET /api/v1/pbx/domains
Domains
Every PBX domain the active org is linked to, plus which one the page opens on. Feeds the Call Flow page's domain picker.
The picker exists because an org with SEVERAL linked domains used to get a blank page: the page follows the org, :func:_default_domain only auto-selects when the choice is unambiguous, so two domains meant an empty default and the "no SIP domain configured" prompt — even though both domains were in scope and reachable by deep link (Ahmed Omar on tc, with ahmed.cloudtools.co.za + pbx.cloudtools.co.za, 2026-09-16).
default is still :func:_default_domain, so a single-domain org and a configured Telephony Settings domain behave exactly as before; it is "" only when the caller has to choose. The frontend renders the picker when there is more than one name and falls back to the first.
names is what the org is LINKED to, never what the caller may reach: an unrestricted caller (staff site / superuser) can still deep-link ?domain= past this list, and _assert_domain_allowed remains the only authorization gate. A default that is outside scope.names is added to the list rather than dropped — that is the "org with no linked domain at all, honour the setting" case :func:_default_domain documents, and the picker must not hide the one domain such an org can open.
200 — Successful Response
json
{
"additionalProperties": true,
"type": "object",
"title": "Response Domains Api V1 Pbx Domains Get"
}GET /api/v1/pbx/call-flow
Call Flow
The full call-flow graph for one domain: inbound numbers, their live routing, and every destination they can point at.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
domain | query | no | string | pbx domain_name; defaults to the org's SIP domain |
200 — Successful Response
json
{
"type": "object",
"additionalProperties": true,
"title": "Response Call Flow Api V1 Pbx Call Flow Get"
}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/pbx/registrations
Registrations
Live SIP registration state for one domain's extensions — the small, cheap read the call-flow canvas polls so its green/red dots track reality without refetching the whole graph (which re-walks every IVR/time-condition over the admin-proxy session).
ok=False means the lookup itself failed (platform key unconfigured, box unreachable, ESL down) — the canvas then keeps showing "unknown" rather than repainting every extension red.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
domain | query | no | string | pbx domain_name; defaults to the org's SIP domain |
200 — Successful Response
json
{
"type": "object",
"additionalProperties": true,
"title": "Response Registrations Api V1 Pbx Registrations Get"
}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/pbx/route
Route
Route an inbound number to a destination (POST /phone-numbers).
Idempotent and non-destructive: portal_pbx.route_number treats an already-correct route as done and REFUSES to re-point a number that's routed elsewhere (the portal can't delete the old route, so a re-point would stack a duplicate) — that refusal comes back as a 409 so the UI can tell the operator to clear the old route on the pbx first.
Request body
json
{
"properties": {
"domain": {
"type": "string",
"title": "Domain",
"description": "pbx domain_name"
},
"number": {
"type": "string",
"title": "Number",
"description": "the inbound DID"
},
"destination": {
"type": "string",
"title": "Destination",
"description": "target extension/ring-group/IVR number"
}
},
"type": "object",
"required": [
"domain",
"number",
"destination"
],
"title": "RouteIn"
}200 — Successful Response
json
{
"additionalProperties": true,
"type": "object",
"title": "Response Route Api V1 Pbx Route Post"
}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/pbx/extension-enabled
Extension Enabled
Enable/disable one extension (PUT /api/v1/extensions/{uuid} via the pbx platform key). Resolves the uuid domain-scoped first.
Request body
json
{
"properties": {
"domain": {
"type": "string",
"title": "Domain"
},
"extension": {
"type": "string",
"title": "Extension"
},
"enabled": {
"type": "boolean",
"title": "Enabled"
}
},
"type": "object",
"required": [
"domain",
"extension",
"enabled"
],
"title": "ExtToggleIn"
}200 — Successful Response
json
{
"additionalProperties": true,
"type": "object",
"title": "Response Extension Enabled Api V1 Pbx Extension Enabled Post"
}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/pbx/softphone
Softphone
SIP credentials for the in-page test phone (extension node → Test phone).
The browser softphone REGISTERs as the extension itself, so it genuinely needs that extension's SIP password — there is no browser-side SIP without it (the pbx box's own /softphone_setup page injects the same values). Guarded the same way every other write here is: an admin login, and the domain must be inside the caller's :class:DomainScope.
POST rather than GET so the extension being tested never lands in an nginx access log or browser history; the response is the secret-bearing half and is marked no-store by the caller.
The transport is deliberately NOT returned: the frontend builds wss://<its own host>/wss. A cross-host WebSocket upgrades (101) but can have its server→client frames dropped in between, which presents as a permanent "Registering" — same-origin is the known-good path.
Request body
json
{
"properties": {
"domain": {
"type": "string",
"title": "Domain",
"description": "pbx domain_name"
},
"extension": {
"type": "string",
"title": "Extension",
"description": "the extension number to register as"
}
},
"type": "object",
"required": [
"domain",
"extension"
],
"title": "SoftphoneIn"
}200 — Successful Response
json
{
"additionalProperties": true,
"type": "object",
"title": "Response Softphone Api V1 Pbx Softphone Post"
}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/pbx/cdrs
Cdrs
Recent call-detail records for one domain (newest first, first page).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
domain | query | no | string | pbx domain_name; defaults to the org's SIP domain |
days | query | no | integer |
200 — Successful Response
json
{
"type": "object",
"additionalProperties": true,
"title": "Response Cdrs Api V1 Pbx Cdrs Get"
}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/pbx/billing-metrics
Billing Metrics
The pbx platform's call-billing rollup — the same figures the pbx admin's own /billing-dashboard page shows (fusionpbx .get_billing_metrics).
Scoped per viewer since 2026-08-27. It used to 403 every non-superuser outright: the box endpoint was platform-wide only, so there was no scoped slice to fall back to and the whole surface was hidden instead. The box now takes ?domains= (see its MetricsScope), so a customer or reseller admin gets the same page over their OWN PBXs — a reseller's covering every sub-account beneath it (viewer_pbx_domains → _org_domain_names).
Two things a scoped caller does NOT get, both deliberate:
- the wholesale half of the payload (
gp/avg_gpp/admin_cost, the anomaly and margin-alert queues) — stripped on the box, so it never crosses the wire, not merely hidden by the page. pbx_urland the per-alert deep links. Those are SSO hops minted WITHOUT adomain_uuidclaim, which lands the holder on the pbx admin with an unscoped operator session — handing one to a customer would give away in one click everything the scoping above just took out.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
period | query | no | string | today|7d|month|30d|90d; server default month |
200 — Successful Response
json
{
"type": "object",
"additionalProperties": true,
"title": "Response Billing Metrics Api V1 Pbx Billing Metrics Get"
}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}