A typed Python SDK for the 3CX XAPI (/xapi/v1), built on httpx and Pydantic v2.
- OAuth2 client-credentials authentication with automatic token refresh
- OData v4 query builder (
$filter,$select,$expand,$orderby,$top,$skip,$count) - Automatic pagination — iterate over all pages with a single generator call
- Full CRUD for users, queues, ring groups, trunks, contacts, and phones
- Reporting helpers for call log, queue performance, extension statistics, activity log, and more
- Context-manager support for clean resource management
- Escape hatches (
client.get()/client.post()) for any unlisted endpoint
pip install 3cx-xapi-python-sdkfrom threecx import ThreeCXClient, ODataQuery
client = ThreeCXClient(
base_url="https://pbx.example.com",
client_id="<service-principal-client-id>",
client_secret="<service-principal-secret>",
)
# ── Active calls ─────────────────────────────────────────────────────────────
for call in client.active_calls.list():
print(f"{call.caller} → {call.callee} (duration: {call.duration}s)")
# Drop a specific call
client.active_calls.drop(call_id=42)
# ── Users ─────────────────────────────────────────────────────────────────────
# List first 50 enabled users, sorted by extension number
q = ODataQuery().filter("Enabled eq true").order_by("Number").top(50)
for user in client.users.list(q):
print(user.number, user.full_name, user.email)
# Get a single user and update their e-mail
user = client.users.get(101)
client.users.update(101, {"Email": "new@example.com"})
# Create a user
from threecx.models import User
new_user = client.users.create(User(
Number="200",
FirstName="Alice",
LastName="Smith",
Email="alice@example.com",
))
# ── Queues ────────────────────────────────────────────────────────────────────
for queue in client.queues.list():
agents = client.queues.get_agents(queue.id)
print(f"Queue {queue.name}: {len(agents)} agents")
# ── Call history ──────────────────────────────────────────────────────────────
q = ODataQuery().filter("CallDirection eq 'Inbound'").order_by("StartTime desc").top(100)
for entry in client.call_history.iterate(q):
print(entry.start_time, entry.caller, "→", entry.callee, entry.status)
# ── Reports ───────────────────────────────────────────────────────────────────
from datetime import datetime, timedelta, timezone
end = datetime.now(timezone.utc)
start = end - timedelta(days=7)
rows = client.reports.get_call_log(start, end)
print(f"{len(rows)} call-log entries for the last 7 days")
# ── System status ─────────────────────────────────────────────────────────────
status = client.system.get_status()
print(f"PBX {status.pbx_version} calls: {status.calls_active} cpu: {status.cpu_usage}%")
# ── Context manager ───────────────────────────────────────────────────────────
with ThreeCXClient(base_url="https://pbx.example.com",
client_id="...", client_secret="...") as c:
print(c.system.get_license())from threecx import ODataQuery
q = (
ODataQuery()
.filter("LastName eq 'Smith'")
.select("Id", "Number", "FirstName", "LastName", "Email")
.expand("Groups")
.order_by("Number")
.top(25)
.skip(0)
.count()
)All query parameters map directly to OData v4 syntax. Refer to the 3CX XAPI swagger for supported fields per endpoint.
- Log in to 3CX as admin.
- Navigate to Admin → Integrations → API.
- Create a new service principal and copy the Client ID and Client Secret.
client.<service> |
Resource |
|---|---|
active_calls |
/ActiveCalls — list & drop live calls |
users |
/Users — CRUD extensions/users |
queues |
/Queues — CRUD call queues & agents |
ring_groups |
/RingGroups — CRUD ring groups |
call_history |
/CallHistoryView — paginated call log |
trunks |
/Trunks, /Peers, /Sbcs |
contacts |
/Contacts — company phone book |
phones |
/Phones, /PhoneTemplates — IP phones |
system |
/SystemStatus, /LicenseStatus, /Parameters |
reports |
Call log, queue & extension statistics, activity log |
Any endpoint not covered by a typed service can be called directly:
data = client.get("/InboundRules", params={"$top": "10"})
result = client.post("/Backups/Pbx.CreateBackup")