FreePBX / Asterisk
The FreePBX adapter is beta-tier . It manages FreePBX 15+ and Asterisk through three independent transports: AMI for real-time call state, ARI for live channel operations, and the FreePBX REST API for configuration. The adapter includes credential encryption, a toll-fraud guard on outbound dial rules, source-IP-gated provisioning, and a shared CircuitBreaker across all three clients.
What works
Section titled “What works”| Area | Details |
|---|---|
| Extensions | List, create, update, delete; password rotation |
| Trunks | SIP/IAX trunk CRUD |
| Ring groups | Create, edit, delete; member management |
| Queues | Create, edit, delete; agent membership |
| IVR menus | Full IVR tree management |
| DIDs (inbound routes) | Create, edit, delete |
| Voicemail | Mailbox config + greeting upload |
| Active calls | Live call list (AMI), hangup channel (AMI) |
| CDR | Call detail records (read) |
| Config apply | Equivalent of the “Apply Config” button |
Not supported: fax (no Asterisk fax stack integration), FreePBX Endpoint Manager templates (commercial module only), WebRTC bridging beyond channel-list view.
The three channels
Section titled “The three channels”| Channel | Protocol | Used for | Port |
|---|---|---|---|
| AMI | Asterisk Manager Interface (TCP text) | Active calls, real-time event stream, hangup channel, call origination | 5038 |
| ARI | Asterisk REST Interface (HTTP + WebSocket) | WebSocket call-event stream; ARI health probe | 8088 |
| REST | FreePBX Admin REST API (HTTP) | Configuration (extensions, trunks, IVR, etc.) | 443 / 80 |
All three credentials are stored inline on the PBX record - there is no separate /api/v1/credentials resource for FreePBX.
Prerequisites
Section titled “Prerequisites”- FreePBX 15 or later with the Admin REST API module installed and enabled.
- AMI user configured in Asterisk (
/etc/asterisk/manager.confor FreePBX AMI Settings). - ARI user configured in
/etc/asterisk/ari.conf. - FreeSDN host IP whitelisted in the FreePBX Admin → Advanced Settings → Allowed Networks (or the source-IP check on the provisioning endpoint will reject requests).
Credentials
Section titled “Credentials”FreePBX does not use the generic /api/v1/credentials store. All three sets of secrets are passed as inline fields when creating (or updating) the PBX record and are encrypted at rest in dedicated Fernet columns on the PBX row (ami_secret_enc, ari_password_enc, web_password_enc). They are never returned by any read endpoint.
| Field | Purpose |
|---|---|
api_username | FreePBX web REST API username (usually admin) |
api_password | FreePBX web REST API password |
settings.ami_username | AMI user (falls back to api_username if omitted) |
settings.ami_secret | AMI secret (stored in ami_secret_enc) |
settings.ari_username | ARI user (falls back to api_username if omitted) |
settings.ari_password | ARI password (stored in ari_password_enc) |
Optionally, for FreePBX 16+ with the Admin REST API module’s M2M application:
| Field | Purpose |
|---|---|
api_client_id | OAuth2 client ID (non-secret, stored in plain column) |
api_client_secret | OAuth2 client secret (stored in api_client_secret_enc) |
When both api_client_id and api_client_secret are supplied the adapter prefers the OAuth2 client-credentials flow over web-session auth.
Adding a PBX
Section titled “Adding a PBX”curl -X POST https://freesdn.example.com/api/v1/voip/pbx \ -H "Cookie: freesdn_access=<token>" \ -H "X-CSRF-Token: <csrf>" \ -H "Content-Type: application/json" \ -d '{ "name": "Main Office PBX", "pbx_type": "freepbx", "ip_address": "pbx.example.com", "api_port": 443, "site_id": "<site-uuid>", "api_username": "admin", "api_password": "<freepbx-admin-password>", "settings": { "ami_username": "freesdn_ami", "ami_secret": "<ami-secret>", "ari_username": "freesdn_ari", "ari_password": "<ari-secret>" } }'After creation, test connectivity:
curl -X POST https://freesdn.example.com/api/v1/voip/pbx/test-connection \ -H "Cookie: freesdn_access=<token>" \ -H "X-CSRF-Token: <csrf>" \ -H "Content-Type: application/json" \ -d '{ "pbx_type": "freepbx", "ip_address": "pbx.example.com", "api_port": 443, "api_username": "admin", "api_password": "<freepbx-admin-password>" }'A healthy response has status: "success" and a details object with ami.connected: true, ari.connected: true, and rest.connected: true.
Common operations
Section titled “Common operations”List extensions
Section titled “List extensions”curl https://freesdn.example.com/api/v1/voip/extensions?pbx_id=<pbx-uuid> \ -H "Cookie: freesdn_access=<token>"Create an extension
Section titled “Create an extension”curl -X POST https://freesdn.example.com/api/v1/voip/pbx/<pbx-uuid>/extensions \ -H "Cookie: freesdn_access=<token>" \ -H "X-CSRF-Token: <csrf>" \ -H "Content-Type: application/json" \ -d '{ "extension_number": "201", "display_name": "Alice Smith", "voicemail_enabled": true }'Active calls
Section titled “Active calls”curl https://freesdn.example.com/api/v1/voip/pbx/<pbx-uuid>/active-calls \ -H "Cookie: freesdn_access=<token>"Hangup a channel
Section titled “Hangup a channel”curl -X POST https://freesdn.example.com/api/v1/voip/pbx/<pbx-uuid>/call/hangup \ -H "Cookie: freesdn_access=<token>" \ -H "X-CSRF-Token: <csrf>" \ -H "Content-Type: application/json" \ -d '{"channel": "<channel-id>"}'Sync / apply pending configuration
Section titled “Sync / apply pending configuration”After making config changes through FreeSDN, trigger a background sync (equivalent to clicking “Apply Config” in the FreePBX web UI). The endpoint returns 202 Accepted immediately; watch progress via pbx.sync.* WebSocket events:
curl -X POST https://freesdn.example.com/api/v1/voip/pbx/<pbx-uuid>/sync \ -H "Cookie: freesdn_access=<token>" \ -H "X-CSRF-Token: <csrf>"Toll-fraud guard
Section titled “Toll-fraud guard”The adapter validates outbound call destinations (originate and transfer) against a blocklist of premium-rate and bare international prefixes (e.g. 1900, 011, satellite ranges). Blocked prefixes can be allow-listed per PBX via pbx.settings.allowed_outbound_prefixes. This check fires in the adapter layer (_check_destination_safe in adapter.py), not the staging layer, and applies only to call origination and transfer - not to trunk configuration.
Frontend pages
Section titled “Frontend pages”- PBXPage - PBX list view
- PBXDetailPage - 11 tabs: Overview, Extensions, Trunks, Ring Groups, Queues, IVR, DIDs, Active Calls, Voicemail, Config, Settings
Backend route prefix
Section titled “Backend route prefix”VoIP module mounts 11 sub-routers:
/api/v1/voip/pbx/*- PBX CRUD + test + apply; trunks, queues, IVR, DIDs, active-calls, and config are sub-paths of/pbx/{pbx_id}//api/v1/voip/extensions/*/api/v1/voip/ring-groups/*/api/v1/voip/call-logs/*- CDR/api/v1/voip/voicemails/*/api/v1/voip/phones/*/api/v1/voip/discovery/*/api/v1/voip/templates/*/api/v1/voip/provisioning/*/api/v1/voip/fleet/*/api/v1/voip/firmware/*
Adapter tier
Section titled “Adapter tier”| CB | RR | TS | SSRF | RG |
|---|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ | ✓ |
Dual-gate writes (DG) are not applicable because PBX config writes go directly through the FreePBX REST API (the PBX itself is the authority, not a FreeSDN staging table). Configuration changes go directly to the FreePBX REST API with no intermediate staging table. Configuration writes (extension, trunk, ring group, IVR, DID, queue CRUD) are applied immediately to the FreePBX REST API - they do not call the read-only gate and are unaffected by ADAPTER_READ_ONLY. Real-time operator actions (originate call, hangup, transfer, config reload) pass through the adapter’s _check_write_allowed guard, but the service layer always supplies force=True, which unconditionally bypasses that gate regardless of the ADAPTER_READ_ONLY env var. As a result, ADAPTER_READ_ONLY=true (the default) does not block any write reachable from the HTTP API in normal operation. Changes are applied immediately to the PBX and logged in the FreeSDN audit trail.