WhatsApp Cloud API SIP Trunk - Couldn't negotiate stream (WebRTC/ICE issue)

Hi all,

I’m stuck on the last step connecting WhatsApp Business Cloud API voice calling to FreePBX 16 / Asterisk 18.9-cert15.

What’s working:

  • Meta’s INVITE is reaching FreePBX :white_check_mark:
  • IP + header matching (X-FB-External-Domain: wa.meta.vc) working :white_check_mark:
  • No authentication errors :white_check_mark:

The error:

res_pjsip_session.c: 442031370091: Couldn't negotiate stream 0:audio-0:audio:sendrecv (nothing)

Root cause:
With pjsip logger on, Meta is sending WebRTC format SDP:

m=audio 3480 UDP/TLS/RTP/SAVPF 111 126
a=group:BUNDLE audio
a=ice-lite
a=candidate:... udp ... typ host

So I need ice_support=yes, webrtc=yes, rtcp_mux=yes on the endpoint. However when I add webrtc=yes to pjsip.endpoint_custom_post.conf and reload, I get:

res_pjsip_config_wizard.c: Unable to load config file 'pjsip_wizard.conf'
sorcery.c: Type 'system' is not reloadable

The reload fails. FreePBX 16 trunk GUI also has no DTLS/WebRTC option for trunks.

Question:
For those with this working on FreePBX — how did you handle the WebRTC/ICE media? Did you configure it outside FreePBX’s control, or is there a way to enable webrtc=yes on a trunk without breaking the config wizard?

Setup: FreePBX 16.0.45 / Asterisk 18.9-cert15, public IP, valid Let’s Encrypt cert.

UPDATE

After some deep debugging, I got the media to negotiate. The key was a combination of manual endpoint overrides and specific Inbound Route signaling.

1. PJSIP Endpoint Overrides Add the following to pjsip.endpoint_custom_post.conf to force the WebRTC/SAVPF profile.

Note: Update the dtls file paths below to point to your specific .crt files found in /etc/asterisk/keys/.

[YOUR_TRUNK_NAME](+)
type=endpoint
context=from-pstn
webrtc=yes
ice_support=yes
use_avpf=yes
rtcp_mux=yes
bundle=yes
media_encryption=dtls
dtls_verify=fingerprint
dtls_setup=actpass
dtls_cert_file=/etc/asterisk/keys/your_domain.crt
dtls_ca_file=/etc/asterisk/keys/your_domain-ca-bundle.crt
media_use_received_transport=yes
rtp_symmetric=yes
rewrite_contact=yes
force_rport=yes
direct_media=no
inband_progress=yes

2. Digit Matching (The “+” hurdle) Meta sends calls in E.164 format (e.g., +xx...). Standard routes looking for local digits often fail to match this.

  • Fix: Create an Inbound Route with the DID pattern _+xxXXXXXXXXXX (the underscore prefix is required for the + to be parsed correctly as a pattern).

3. Signaling Fix (The “Ghost Answer”) Even after media negotiated, the caller side would show “Answered” while the extension didn’t ring.

  • Fix: In the Inbound Route > Advanced tab, set Signal RINGING to Yes. This prevents the PBX from sending an immediate 200 OK during the WebRTC handshake, allowing the extension to ring first.

Result: Two-way audio and correct extension ringing established.


UPDATE - FULLY WORKING CONFIGURATION

I finally got this working with two-way audio and no call drops. Just completed a 2-minute test call with perfect audio quality. Updating this thread with the complete solution.

The key fix was dtls_auto_generate_cert=yes instead of pointing to certificate files. Meta’s servers weren’t trusting my CA, causing “tlsv1 alert unknown ca” errors.

Working Trunk Configuration:

GUI Settings (Connectivity → Trunks → pjsip Settings):

General tab:

  • Authentication: None

  • Registration: None

  • SIP Server: wa.meta.vc

  • SIP Server Port: 5061

  • Context: from-pstn

  • Transport: 0.0.0.0-tls

Advanced tab:

  • RTP Symmetric: Yes

  • Force rport: Yes

  • Rewrite Contact: No

  • Direct Media: No

  • Qualify Frequency: 0

Custom File (/etc/asterisk/pjsip.endpoint_custom_post.conf):

[YOUR_WHATSAPP_NUMBER](+)
type=endpoint
context=from-pstn
webrtc=yes
dtls_auto_generate_cert=yes
rtp_symmetric=yes
force_rport=yes
direct_media=no
rewrite_contact=no

Inbound Route:

  • DID Number: _+YOUR_WHATSAPP_NUMBER (with underscore and plus)

  • Signal RINGING: Yes (Advanced tab)

  • Destination: IVR, Time Condition, Ring Group, etc.

What finally worked vs. what didn’t:

Setting Didn’t Work Worked
DTLS Certs dtls_cert_file=/path/to/cert.pem dtls_auto_generate_cert=yes
DTLS Verify dtls_verify=fingerprint Not needed with auto-generate
Rewrite Contact Yes No

Hope this helps anyone else stuck on this!

So what’s the purpose of this integration? Just curious

It allows WhatsApp voice calls to hit our IVR and queues directly — just like any other SIP trunk. We get call records, routing, and agents don’t need a physical mobile phone to receive WhatsApp calls. It’s all handled through the PBX.

I bet it also allows WhatsApp to record all your calls as well and then use it to train their LLM and voice recognition engines. I wouldn’t trust anything Meta