3CX Python SDK
A typed Python SDK for the 3CX XAPI (/xapi/v1), built on httpx and Pydantic v2.
Features
- 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 coverage — 37 typed services covering every resource group in the 3CX XAPI swagger spec
- Auto-generated models — every Pydantic model is generated from
swagger.yaml, giving you full IDE autocomplete on every field of every entity (User has 60+ typed fields, Queue has 50+, etc.)
- 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 raw access to any endpoint
Installation
pip install 3cx-xapi-python-sdk
Quick start
from threecx import ThreeCXClient, ODataQuery
client = ThreeCXClient(
base_url="https://pbx.example.com",
client_id="<service-principal-client-id>",
client_secret="<service-principal-secret>",
)
for call in client.active_calls.list():
print(f"{call.caller} → {call.callee} ({call.status} since {call.established_at})")
client.active_calls.drop(call_id=42)
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_address)
user = client.users.get(101)
client.users.update(101, {"EmailAddress": "new@example.com"})
from threecx.models import User
new_user = client.users.create(User(
Number="200",
FirstName="Alice",
LastName="Smith",
EmailAddress="alice@example.com",
))
for queue in client.queues.list():
agents = client.queues.get_agents(queue.id)
print(f"Queue {queue.name}: {len(agents)} agents")
q = ODataQuery().filter("SrcExternal eq true").order_by("SegmentStartTime desc").top(100)
for entry in client.call_history.iterate(q):
print(entry.segment_start_time, entry.src_caller_number, "→", entry.dst_caller_number, entry.call_answered)
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")
status = client.system.get_status()
print(f"PBX {status.version} calls: {status.calls_active} disk: {status.disk_usage}%")
with ThreeCXClient(base_url="https://pbx.example.com",
client_id="...", client_secret="...") as c:
print(c.system.get_license())
OData query builder
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.
Every method whose endpoint documents query options takes query=, including single-entity and settings getters
($select/$expand), e.g. client.settings.get_mail_settings(query=ODataQuery().select("MailServer")). list() methods
return the items of one page; read $count with client.call_history.count(q), or through the escape hatch:
client.get("/Users", params=ODataQuery().count().top(0).to_params())["@odata.count"].
Calling actions
3CX actions (POST …/Pbx.Name) take named parameters, and the SDK methods take the same parameters as arguments. Pass
the object itself; the method posts it under the parameter name 3CX expects (InstallUpdates wants
{"install": {...}}, SetUpdateSettings wants {"settings": {...}}, and so on).
from threecx.models import InstallUpdates, XServiceManageOptions
available = client.updates.get_updates()
client.updates.install_updates(InstallUpdates(
key=available["Key"],
entries=[item["Guid"] for item in available["Entries"]],
))
client.pbx_services.restart(XServiceManageOptions(service_names=["3CXQueueManager"]))
client.pbx_services.stop({"ServiceNames": ["3CXQueueManager"]})
Object arguments accept the model from threecx.models or a plain dict; enum arguments accept the enum or its string
value. Actions without parameters take no body argument: client.recordings.archive().
Creating a service principal
- Log in to 3CX as admin.
- Navigate to Admin → Integrations → API.
- Create a new service principal and copy the Client ID and Client Secret.
Services reference
The SDK exposes 37 services as attributes of ThreeCXClient, covering every resource group in swagger.yaml.
Calls & telephony
active_calls | /ActiveCalls — list & drop live calls |
call_history | /CallHistoryView — paginated call log |
recordings | /Recordings, /RemoteArchivingSettings |
voicemail | /VoicemailSettings, /MusicOnHoldSettings |
fax | /Fax, /FaxServerSettings |
Users, groups & directories
users | /Users — CRUD extensions/users + greetings, phones, provisioning |
my_user | /MyUser — current authenticated user |
groups | /Groups — full admin groups (members, rights, restrictions) |
my_group | /MyGroup — current user's primary group |
contacts | /Contacts — company phone book |
parameters | /TenantProperties, /DNProperties, directory info |
Call routing & flow
queues | /Queues — CRUD call queues & agents |
ring_groups | /RingGroups — CRUD ring groups |
inbound_rules | /InboundRules — DID/inbound rules |
outbound_rules | /OutboundRules — outbound dial rules |
receptionists | /Receptionists — digital receptionists |
holidays | /Holidays, /OfficeHours |
parkings | /Parkings, /CallParkingSettings |
call_flow | /CallFlowApps, /CallFlowScripts |
Trunks & PBX hardware
trunks | /Trunks, /TrunkTemplates, /Peers, /Sbcs |
phones | /PhoneTemplates, /SipDevices, /Fxs, /FxsTemplates, /DeviceInfos, /Firmwares |
Communication
chat | /ChatHistoryView, /ChatMessagesHistoryView |
prompts | /PromptSets, /CustomPrompts, /Playlists |
email | /EmailTemplate |
System administration
system | /SystemStatus, /LicenseStatus, /Parameters |
settings | /GeneralSettingsForPbx, /MailSettings, /CDRSettings, ~20 settings domains |
pbx_services | /Services — start/stop/enable/disable PBX services |
backups | /Backups, failover & restore settings |
updates | /GetUpdates(), /InstallUpdates, update settings, prompt set / CRM updates, purge |
event_logs | /EventLogs |
security | /SecurityTokens, /ServicePrincipals, /BlackListNumbers, /Blocklist, /AntiHackingSettings, /Firewall |
emergency | /EmergencyGeoLocations, /EmergencyNotificationsSettings |
defs | /Defs — codecs, timezones, gateway parameters, countries, DID numbers |
Integrations
integrations | /Microsoft365Integration, /Microsoft365TeamsIntegration, /GoogleSettings, /AmazonIntegrationSettings, /DataConnectorSettings, /AISettings |
crm | /CrmIntegration, /CrmTemplates |
website_links | /WebsiteLinks |
Reporting
reports | 30+ report endpoints: call log, queue & extension statistics, agent login, activity log, SLA breaches, audit, scheduled reports |
Escape hatch
Any endpoint not covered by a typed service can be called directly:
data = client.get("/InboundRules", params={"$top": "10"})
client.post("/Backups/Pbx.Backup", json={"description": {"Name": "before-upgrade"}})
client.post() sends json exactly as given, so key an action's body by its parameter names yourself (look them up
in swagger.yaml under the action's …RequestBody schema).