Registers a new `three_cx` provider that fronts a 3CX cloud PBX through an intermediate Asterisk bridge. Save-time hook writes the matching PJSIP endpoint/aor/auth/registration and dialplan rows to the Asterisk Realtime Architecture Postgres (via `ASTERISK_ARA_DSN`), so a config change in the Dograh UI is immediately picked up by Asterisk without a `pjsip reload`. Strip prefix is honoured at the dialplan layer. Inbound calls are matched back to a configuration by the dialled extension (`account_id_credential_field="extension"`), allowing one shared Asterisk to serve multiple Dograh orgs without collision. Touches `providers/__init__.py` and `schemas/telephony_config.py` only — per `providers/AGENTS.md`. Provider/transport/strategies are duplicated from `ari/` rather than imported, in line with the cross-provider-import prohibition. See `docs/providers/three_cx.md` for the Asterisk ARA setup runbook. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.3 KiB
3CX Telephony Provider
Connect a Dograh AI agent to a 3CX cloud PBX through an intermediate Asterisk bridge. The Asterisk box terminates the SIP/RTP leg toward 3CX and exposes a standard ARI + externalMedia surface to Dograh — identical to the Asterisk ARI provider plus an automated trunk-provisioning step.
+-------------------+ SIP/RTP +-------------+ ARI REST + +---------+
| 3CX cloud SBC | <------------> | Asterisk | WS audio | -> | Dograh |
| 1156.3cx.cloud | | (PJSIP ARA)| | | agent |
+-------------------+ +-------------+ + +---------+
^
| ps_endpoints, ps_aors,
| ps_auths, ps_registrations,
| extensions (Postgres)
|
+----+----+
| Dograh |
| save UI |
+---------+
When an admin saves a 3CX TelephonyConfiguration in the Dograh UI, the
provider's preprocess_credentials_on_save hook writes the matching
PJSIP endpoint/aor/auth/registration rows and the +39-stripping
dialplan into the Asterisk Realtime Architecture (ARA) Postgres. Asterisk
picks them up dynamically — no pjsip reload needed.
§1 — Asterisk side prerequisites
The bridging Asterisk must be ≥ Asterisk 18 with PJSIP and ARA enabled
against the same Postgres Dograh writes to. One Asterisk instance can
serve many Dograh 3CX configurations (multi-tenant) because each trunk
gets a unique endpoint id of the form dograh_<slug(sip_domain)>_<extension>.
1.1 Postgres tables
Run the standard Asterisk realtime DDL on the ARA database — the
relevant tables are ps_auths, ps_aors, ps_endpoints,
ps_registrations, ps_transports, ps_globals, and extensions.
The canonical schema ships with Asterisk under contrib/realtime/postgresql/.
1.2 res_config_pgsql.conf
[asterisk]
type = pgsql
hostname = postgres.internal
dbname = asterisk_ara
user = asterisk_ro
password = ********
port = 5432
requirements = warn
1.3 sorcery.conf
[res_pjsip]
endpoint = realtime,ps_endpoints
auth = realtime,ps_auths
aor = realtime,ps_aors
domain_alias = realtime,ps_domain_aliases
contact = realtime,ps_contacts
[res_pjsip_endpoint_identifier_ip]
identify = realtime,ps_endpoint_id_ips
[res_pjsip_outbound_registration]
registration = realtime,ps_registrations
1.4 extconfig.conf
[settings]
ps_endpoints = pgsql,asterisk
ps_auths = pgsql,asterisk
ps_aors = pgsql,asterisk
ps_registrations = pgsql,asterisk
extensions = pgsql,asterisk
1.5 Static PJSIP transport
Dograh writes endpoints that reference a transport by name (default:
transport-udp). Define it once in pjsip.conf:
[transport-udp]
type = transport
protocol = udp
bind = 0.0.0.0:5060
1.6 Stasis app + externalMedia
; ari.conf
[general]
enabled = yes
[dograh]
type = user
read_only = no
password = <ARI password to paste in the Dograh UI>
; websocket_client.conf
[dograh_staging]
type = websocket_client
uri = ws://dograh-backend:8000/api/v1/telephony/ws/ari
protocols = media
connection_type = persistent
Start the Stasis app and confirm registration is happening:
asterisk -rx "module reload res_pjsip.so"
asterisk -rx "pjsip show registrations"
§2 — Dograh side prerequisites
Set the connection string to the ARA Postgres in api/.env:
ASTERISK_ARA_DSN=postgresql://dograh_rw:********@postgres.internal:5432/asterisk_ara
The user needs SELECT, INSERT, UPDATE, DELETE on ps_auths,
ps_aors, ps_endpoints, ps_registrations, and extensions. No DDL
permissions required at runtime.
Restart the Dograh API process after setting the env var.
§3 — Per-trunk flow in the Dograh UI
For each 3CX tenant + extension Dograh should serve:
-
Open Telephony Configurations → Add → select 3CX (Asterisk bridge).
-
Fill in the form:
Field Value (example) ARI Endpoint http://asterisk.internal:8088Stasis App Name dograhARI Password (matches ari.conf[dograh]password)websocket_client.conf Name dograh_staging3CX SIP Domain 1156.3cx.cloud3CX Extension 12611SIP Password (from ~/.claude-phone/.envor 3CX admin console)Strip Prefix (regex) ^\+39From Numbers +393331112222 -
Save. On save the
preprocess_credentials_on_savehook writes the six-table ARA set in a single transaction. A failure aborts the save withHTTP 502and a message describing which write failed; nothing persists.
§4 — Verification
Confirm the trunk landed in ARA:
psql $ASTERISK_ARA_DSN -c \
"SELECT id FROM ps_endpoints WHERE id LIKE 'dograh\\_%'"
psql $ASTERISK_ARA_DSN -c \
"SELECT id, server_uri FROM ps_registrations WHERE id LIKE 'dograh\\_%'"
Confirm Asterisk has registered upstream with 3CX:
asterisk -rx "pjsip show registrations"
# Expect: <id> <server> Registered
Originate a test outbound call from Dograh and verify the +39 prefix
was stripped on the way out:
asterisk -rx "core set verbose 4"
# In another terminal: trigger an outbound from the Dograh API.
# In the Asterisk console you should see:
# Dial: PJSIP/3331112222@dograh_1156_3cx_cloud_12611
# i.e. without '+39'.
Known limitations
- The hook only supports the literal
^\+<digits>regex form forstrip_prefix. PCRE alternation isn't translated to Asterisk's ad-hoc extension pattern syntax. Adding a[02-9]or branching regex needs an extension todialplan._prefix_to_pattern. - Deprovisioning on TelephonyConfiguration deletion is not currently
wired.
provisioning._deprovision_3cx_trunkexists as a callable but no registry hook fires it; admin tooling can call it directly. Filed for follow-up rather than in scope for the initial provider. transport_nameis hard-coded totransport-udp(overridable per credentials dict). TLS or TCP trunks toward 3CX need the admin to define the transport and pass the name through.