Skip to content

pbx ​

← All modules

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

NameInRequiredTypeDescription
domainquerynostringpbx 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

NameInRequiredTypeDescription
domainquerynostringpbx 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

NameInRequiredTypeDescription
domainquerynostringpbx domain_name; defaults to the org's SIP domain
daysquerynointeger

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_url and the per-alert deep links. Those are SSO hops minted WITHOUT a domain_uuid claim, 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

NameInRequiredTypeDescription
periodquerynostringtoday|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"
}

Lubb One Documentation