dograh/docs/providers/three_cx.md
stefandsl 533a873ab7 feat: add 3CX telephony provider with Asterisk ARA provisioning
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>
2026-05-26 13:07:50 +02:00

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:

  1. Open Telephony ConfigurationsAdd → select 3CX (Asterisk bridge).

  2. Fill in the form:

    Field Value (example)
    ARI Endpoint http://asterisk.internal:8088
    Stasis App Name dograh
    ARI Password (matches ari.conf [dograh] password)
    websocket_client.conf Name dograh_staging
    3CX SIP Domain 1156.3cx.cloud
    3CX Extension 12611
    SIP Password (from ~/.claude-phone/.env or 3CX admin console)
    Strip Prefix (regex) ^\+39
    From Numbers +393331112222
  3. Save. On save the preprocess_credentials_on_save hook writes the six-table ARA set in a single transaction. A failure aborts the save with HTTP 502 and 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 for strip_prefix. PCRE alternation isn't translated to Asterisk's ad-hoc extension pattern syntax. Adding a [02-9] or branching regex needs an extension to dialplan._prefix_to_pattern.
  • Deprovisioning on TelephonyConfiguration deletion is not currently wired. provisioning._deprovision_3cx_trunk exists 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_name is hard-coded to transport-udp (overridable per credentials dict). TLS or TCP trunks toward 3CX need the admin to define the transport and pass the name through.