Asterisk 22 & ARI: externalMedia command succeeds but WebSocket connection is never initiated

I am trying to build a real-time voice assistant using Asterisk 22 and the Asterisk REST Interface (ARI). My goal is to stream live call audio to a Python WebSocket server for speech recognition using the externalMedia feature.

The Problem:

My Python ARI application successfully sends the POST /ari/channels/externalMedia command. The Asterisk REST API returns a “200 OK” status, and the Python logs show that the bridge and media channel are created successfully. However, Asterisk never attempts to connect to my WebSocket server running on ws://127.0.0.1:8089. The handler in my Python script which should be triggered by a new WebSocket connection is never called.

My Environment:

  • Asterisk Version: 22.4.1
  • OS: Ubuntu
  • ARI Client: Python with requests and websockets libraries.

What I Have Verified:

  1. WebSocket Server is Working: I have written a separate, simple Python WebSocket client and successfully connected to the server on port 8089 while my main ARI application is running. This proves the server is running correctly.
  2. No Firewall Issue: The successful connection test from a separate client on the same machine confirms that there is no local firewall (like ufw) blocking port 8089.
  3. Clean Bridge/Channel Logic: My Python code first creates a mixing bridge, adds the main call channel, and then creates the externalMedia channel while specifying the bridgeId during creation. The Asterisk logs show all these steps completing without any errors.

The Core Question:

Given that all ARI commands succeed and there are no network/firewall issues, what could be preventing Asterisk from physically initiating the WebSocket client connection to the external_host? Is there a known issue, a missing module (format_slin, etc.), or a subtle PJSIP configuration that could cause this “silent failure” in Asterisk 22?

Below are my cleaned-up configuration files and the final “successful-looking but not working” Asterisk log. Any help or ideas would be greatly appreciated.

    def make_call(self, number):
        data = {
            "endpoint": f"PJSIP/{number}@netgsm_trunk",
            "context": "outbound-netgsm",
            "extension": number,
            "priority": 1,
            "callerId": "xxxxxx",
            "timeout": 30,
            "app": "netgsm-realtime",
            "appArgs": number
        }
        
        try:
            response = self.session.post(f"{ARI_URL}/channels", json=data)
            if response.status_code == 200:
                channel_info = response.json()
                channel_id = channel_info.get('id')
                return channel_id
            else:
                print(f"❌ Call Error: {response.status_code} - {response.text}")
                return None
        except Exception as e:
            print(f"❌ Call Error: {e}")
            return None
def start_speech_recognition(self, channel_id):
    print(f"🎙️ Starting speech recognition for {channel_id}")
    try:
        bridge_response = self.session.post(f"{ARI_URL}/bridges", json={"type": "mixing"})
        bridge_response.raise_for_status()
        bridge_id = bridge_response.json().get('id')
        print(f"✅ Bridge created: {bridge_id}")

        self.session.post(f"{ARI_URL}/bridges/{bridge_id}/addChannel", json={"channel": channel_id})
        print(f"✅ Main channel ({channel_id}) added to bridge.")

        external_host = "127.0.0.1:8089"
        external_media_response = self.session.post(
            f"{ARI_URL}/channels/externalMedia",
            json={
                "app": "netgsm-realtime",
                "bridgeId": bridge_id,
                "external_host": external_host,
                "format": "slin"
            }
        )
        external_media_response.raise_for_status()
        external_channel_id = external_media_response.json().get('id')
        print(f"✅ External Media channel created ({external_channel_id}) and added to bridge.")

        self.active_calls[channel_id] = {
            'start_time': datetime.now(),
            'is_recognizing': True,
            'transcript_file': f"/tmp/call_transcript_{channel_id}.txt",
            'external_channel_id': external_channel_id,
            'bridge_id': bridge_id
        }
        print(f"✅ Speech recognition started successfully: {channel_id}")

    except requests.exceptions.HTTPError as e:
        print(f"❌ HTTP Error (start_speech_recognition): {e.response.status_code} - {e.response.text}")
    except Exception as e:
        print(f"❌ General error while starting speech recognition: {e}")

pjsip.conf

[transport-ws]
type=transport
protocol=ws
bind=0.0.0.0:8088

[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0:5060

[netgsm_trunk_auth]
type=auth
auth_type=userpass
username=xxxxxx
password=xxxxxx
realm=sip.netgsm.com.tr

[netgsm_trunk_aor]
type=aor
contact=sip:xxxxxx@sip.netgsm.com.tr:5060
qualify_frequency=60

[netgsm_trunk]
type=endpoint
context=outbound-netgsm
disallow=all
allow=ulaw
allow=alaw
allow=slin
aors=netgsm_trunk_aor
auth=netgsm_trunk_auth
outbound_auth=netgsm_trunk_auth
force_rport=yes
rewrite_contact=yes
transport=transport-udp
from_user=xxxxxxxx
from_domain=sip.netgsm.com.tr

[netgsm_registration]
type=registration
transport=transport-udp
outbound_auth=netgsm_trunk_auth
server_uri=sip:sip.netgsm.com.tr:5060
client_uri=sip:xxxxxx@sip.netgsm.com.tr
contact_user=xxxxxxxx
expiration=3600
retry_interval=60
max_retries=10

extentions.conf (for outbound calls)

[outbound-netgsm]
exten => _X.,1,NoOp(Giden Arama: ${CALLERID(num)} -> ${EXTEN})
exten => _X.,n,Set(CHANNEL(language)=tr) ;
exten => _X.,n,Dial(PJSIP/${EXTEN}@netgsm-trunk,30,tTr) ;
exten => _X.,n,GotoIf($["${DIALSTATUS}" = "ANSWER"]?answered:notanswered) ;

exten => _X.,n(answered),Stasis(netgsm-realtime,${EXTEN}) ;
exten => _X.,n,Hangup()

exten => _X.,n(notanswered),NoOp(Arama yanıtlanmadı.)
exten => _X.,n,Hangup()

Asterisk CLI output once calling is ongoing:

tahsilist-virtual-machine*CLI> core set debug 5
Core debug is still 5.
tahsilist-virtual-machine*CLI> core set verbose 5
Console verbose was 3 and is now 5.
    -- Called 5345164540@netgsm_trunk
       > 0x7f404c2f0710 -- Strict RTP learning after remote address set to: 185.88.7.206:11866
    -- PJSIP/netgsm_trunk-00000001 is making progress
    -- PJSIP/netgsm_trunk-00000001 answered
       > Launching Stasis(netgsm-realtime,5345164540) on PJSIP/netgsm_trunk-00000001
    -- <PJSIP/netgsm_trunk-00000001> Playing 'tr/test-voice.slin' (language 'en')
       > 0x7f404c2f0710 -- Strict RTP switching to RTP target address 185.88.7.206:11866 as source
       > 0x7f404c2f0710 -- Strict RTP learning complete - Locking on source address 185.88.7.206:11866
    -- Channel PJSIP/netgsm_trunk-00000001 joined 'simple_bridge' stasis-bridge <84249a77-76a3-4c15-ade0-20298fef21ce>
       > 0x7f3f78014040 -- Strict RTP learning after remote address set to: 127.0.0.1:8089
    -- Called 127.0.0.1:8089/c(slin)
    -- UnicastRTP/127.0.0.1:8089-0x7f3f780130b0 answered
       > Launching Stasis(netgsm-realtime) on UnicastRTP/127.0.0.1:8089-0x7f3f780130b0
    -- Channel PJSIP/netgsm_trunk-00000001 left 'simple_bridge' stasis-bridge <84249a77-76a3-4c15-ade0-20298fef21ce>

This is an easy thing to answer. It’s not working because the code doesn’t support it. If something told you it should work, it lied. There’s a pull request up to add such support, but it has not yet been merged and would land in a future release. Right now the media would be sent as RTP over UDP from Asterisk to the IP address and port you give to externalMedia.

As well there is no “bridgeId” field to externalMedia[1], so that would do absolutely nothing.

[1] Channels - Asterisk Documentation

Thank you for your previous response, which was helpful. You clarified that externalMedia sends audio via RTP over UDP, not WebSockets. This explained why my WebSocket approach was failing.

Based on your feedback, I have abandoned the externalMedia ARI command for now. Instead, I’ve focused on using the more direct dialplan method you mentioned: forking the audio with RTP_SENDTO.

Unfortunately, I am still stuck, but the problem is now much more specific.

My Current (Failing) Architecture:

  1. A minimal Python script using ARI (ari_caller.py) does only one thing: originates the outbound call and immediately exits. It does not listen for events.
  2. My extensions.conf takes the call, answers it, and then uses Gosub to run a subroutine that executes Set(RTP_SENDTO=127.0.0.1:15016).
  3. A separate Python script (udp_listener.py) binds to 127.0.0.1:15016 and waits for the UDP packets.

What Works:

  • The call is placed and answered successfully.
  • The Asterisk CLI log shows that the dialplan is executed perfectly. The Gosub runs, and the Set(RTP_SENDTO=...) command is executed without any errors or warnings.

The Core Problem: Despite the Asterisk log showing a successful execution of Set(RTP_SENDTO=...), absolutely no UDP packets are ever sent to 127.0.0.1:15016.

Here is everything I have tried and verified to debug this “silent failure”:

  1. Forced Asterisk into the Media Path: My pjsip.conf for the trunk endpoint includes direct_media=no and rtp_symmetric=no. This should ensure Asterisk is not bypassing the media stream.
  2. Verified with tcpdump: This is the most crucial test. I ran sudo tcpdump -i lo -n udp port 15016 while the call was active. It showed zero packets. This confirms with 100% certainty that Asterisk is not sending any data, and the issue is not with my Python listener script.
  3. Verified Codecs & Formats:
  • module show like codec confirms that codec_ulaw.so and codec_alaw.so are loaded and running. Asterisk can understand the incoming audio from the provider.
  • module show like format now confirms format_sln.so is running.

My Final Question:

Given that:

  • The dialplan executes Set(RTP_SENDTO) without error.
  • tcpdump proves no packets are being sent.
  • direct_media is disabled.
  • All necessary codec and format modules appear to be loaded correctly.

What else could possibly cause Asterisk to silently fail to send the RTP stream? Is there a core setting, a PJSIP endpoint option I’m missing, or a known issue/bug in Asterisk 22’s RTP forking mechanism that could lead to this behavior?

Thank you again for your time and expertise. I am truly stuck and would appreciate any further ideas.

pjsip.conf:

[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0:5060

[netgsm_trunk_auth]
type=auth
auth_type=userpass
username=8503464493
password=12aadc2f6e5f
realm=sip.netgsm.com.tr

[netgsm_trunk_aor]
type=aor
contact=sip:8503464493@sip.netgsm.com.tr:5060
qualify_frequency=60

[netgsm_trunk]
type=endpoint
context=outbound-netgsm
disallow=all
allow=ulaw
allow=alaw
allow=slin
aors=netgsm_trunk_aor
auth=netgsm_trunk_auth
outbound_auth=netgsm_trunk_auth
force_rport=yes
rewrite_contact=yes
transport=transport-udp
from_user=8503464493
from_domain=sip.netgsm.com.tr
direct_media=no
rtp_symmetric=no

[netgsm_registration]
type=registration
transport=transport-udp
outbound_auth=netgsm_trunk_auth
server_uri=sip:sip.netgsm.com.tr:5060
client_uri=sip:8503464493@sip.netgsm.com.tr
contact_user=8503464493
expiration=3600
retry_interval=60
max_retries=10

extentions.conf

[outbound-netgsm]
exten => _X.,1,NoOp(Giden Arama: ${CALLERID(num)} -> ${EXTEN})
exten => _X.,n,Set(CHANNEL(language)=tr)
exten => _X.,n,Answer()
exten => _X.,n,Wait(1)
exten => _X.,n,Gosub(sub-rtp-fork,s,1)
exten => _X.,n,Wait(3600)
exten => _X.,n,Hangup()


[sub-rtp-fork]
exten => s,1,NoOp(Gosub rtp-fork calisiyor. Ses 127.0.0.1:15016 adresine gonderilecek.)
exten => s,n,Set(RTP_SENDTO=127.0.0.1:15016)
exten => s,n,Return()

Asterisk Log:

tahsilist-virtual-machine*CLI> core set debug 5
Core debug was OFF and is now 5.
tahsilist-virtual-machine*CLI> core set verbose 5
Console verbose was 3 and is now 5.
    -- Called 5345164540@netgsm_trunk
       > 0x7fa30c2f1230 -- Strict RTP learning after remote address set to: 185.88.7.206:12500
    -- PJSIP/netgsm_trunk-00000000 is making progress
    -- PJSIP/netgsm_trunk-00000000 answered
    -- Executing [5345164540@outbound-netgsm:1] NoOp("PJSIP/netgsm_trunk-00000000", "Giden Arama: 8503464493 -> 5345164540") in new stack
    -- Executing [5345164540@outbound-netgsm:2] Set("PJSIP/netgsm_trunk-00000000", "CHANNEL(language)=tr") in new stack
    -- Executing [5345164540@outbound-netgsm:3] Answer("PJSIP/netgsm_trunk-00000000", "") in new stack
    -- Executing [5345164540@outbound-netgsm:4] Wait("PJSIP/netgsm_trunk-00000000", "1") in new stack
    -- Executing [5345164540@outbound-netgsm:5] Gosub("PJSIP/netgsm_trunk-00000000", "sub-rtp-fork,s,1") in new stack
    -- Executing [s@sub-rtp-fork:1] NoOp("PJSIP/netgsm_trunk-00000000", "Gosub rtp-fork calisiyor. Ses 127.0.0.1:15016 adresine gonderilecek.") in new stack
    -- Executing [s@sub-rtp-fork:2] Set("PJSIP/netgsm_trunk-00000000", "RTP_SENDTO=127.0.0.1:15016") in new stack
    -- Executing [s@sub-rtp-fork:3] Return("PJSIP/netgsm_trunk-00000000", "") in new stack
    -- Executing [5345164540@outbound-netgsm:6] Wait("PJSIP/netgsm_trunk-00000000", "3600") in new stack

“RTP_SENDTO” doesn’t exist. There is no “RTP forking” using it. You’ve been lied to, again.

What a shame! this is what happens when you don’t understand how the tech works but rely on AI advices.

Another way to send/receive audio between an external program/script and Asterisk is via an AudioSocket connection.

I have an example of how to do this in the fastagi_audio_player_async script in my Seaskirt Examples repo.

Thank you for your response. I found a community creating virtual assitant with Asterisk and AudioSocket thanks to your idea. If you are also insterested in the community here is the link: