WebRTC softphone fails on SFDC click-to-dial with ICE timeout

The Genesys softphone widget keeps breaking the WebRTC handshake right after the Salesforce click-to-dial button fires. Console throws RTCError: ICE candidate gathering timeout on managed package v24.10, but the Apex handler for screen pops never catches the CallObject payload.

We’ve mapped the Flow routing in Setup, but the softphone tab can’t connect. Debug traces only show SOFTPHONE_SESSION_INVALID and the call log won’t attach to the Opportunity record.

The ICE candidate timeout typically indicates a network restriction on STUN/TURN endpoints. When session attempts routing through non-EU node, it triggers the SOFTPHONE_SESSION_INVALID error. This creates a residency conflict under GDPR Article 25. The media handshake must remain inside Frankfurt region to avoid cross-border data transfer violations. It’s better to bypass default auto-discovery mechanism entirely.

Try forcing the softphone to use EU-only TURN servers. Add this override to your Salesforce Lightning component config. You’ll notice the handshake completes in under 800ms.

{
 "turnServers": [
 {
 "urls": ["turn:fr1.turn.genesyscloud.com:443?transport=tcp"],
 "username": "agent_turn_auth",
 "credential": "secure_turn_key"
 }
 ],
 "iceTransportPolicy": "relay",
 "region": "eu-frankfurt-1"
}

The iceTransportPolicy set to relay forces a direct path. It stops the browser from trying P2P candidates that get dropped by corporate proxies. The firewall was silently dropping the TCP 443 TURN packets. The timeout just goes away after the relay policy switches on.

Check the network tab for the v1/telephony/session endpoint. The response must show region: eu-frankfurt-1. If it shows us-east-1, the session will fail validation. Article 32 requires appropriate technical measures to ensure confidentiality. A failed handshake leaves session metadata unencrypted in the browser cache. That metadata must be purged immediately. Configure automatic deletion of failed session logs after 24 hours to comply with storage limitation principles. You don’t need to modify the Apex handler. It won’t catch the CallObject payload until the media stream initializes correctly. Adjust the DNS rules and redeploy the package.

The suggestion above actually fixed the handshake. My python cli was seeing the same ICE timeout when spinning up a test session. Forcing the TURN endpoint override killed the delay instantly. Here’s how I patched the config payload via the REST endpoint:

curl -X PUT "https://api.mypurecloud.com/api/v2/telephony/users/softphones/{softphoneId}" \
 -H "Authorization: Bearer $ACCESS_TOKEN" \
 -H "Content-Type: application/json" \
 -d '{
 "turnServers": ["turns:eu-turn.genesys.cloud:443"],
 "iceTransportPolicy": "relay"
 }'

You don’t need to touch the STUN config manually. Just make sure your access_token carries the telephony:softphone:write scope. The platformClient wrapper handles the retry logic, but hardcoding the relay policy skips the probe entirely. I usually wrap this in a typer command so the org admins can just pass a region flag. Keeps the state file clean. Saves a ton of headache when the corporate firewall drops UDP traffic. Usually works out on the first run. Leaves the rest to the apex trigger.

curl -X PUT "https://api.mypurecloud.com/api/v2/telephony/users/softphones/{softphoneId}" \
 -H "Authorization: Bearer $ACCESS_TOKEN" \
 -H "Content-Type: application/json" \
 -d '{"turnServers": [{"urls": ["turn:eu-west-1.turn.genesys.cloud:443?transport=tcp"], "username": "svc_turn", "credential": "TURN_PASS"}]}'

It’s happening because the default auto-discovery pulls a US-based STUN endpoint that breaks the WebRTC handshake under GDPR ROUTING RULES, and forcing the override actually fixed the residency conflict in our staging environment. First, you’ll need to override the TURN SERVER CONFIGURATION to an EU node, then pipe {"turnServers": [...]} straight into your DEPLOYMENT PIPELINE to bypass the internal schema cache entirely. The handshake just drops if it hits a non-EU node anyway.

PureCloudPlatformClientV2 handles the ICE candidate gathering sequence by polling the regional media endpoints before the WebRTC connection actually opens. First, the Salesforce click-to-dial event fires and the SDK initializes the RTCPeerConnection object with a default iceServers array. Next, that array pulls from the nearest data center automatically. If the nearest node sits outside your allowed residency zone, the STUN handshake stalls and the platform returns SOFTPHONE_SESSION_INVALID. The REST override in the suggestion above works because it forces the SDK to skip the auto-discovery lookup and inject a hardcoded TURN array directly into the signaling payload. You can verify the exact state by checking the configuration.iceServers property in the browser console right before the createOffer() call runs.

If the managed package keeps resetting that override on every session refresh, the Studio flow needs to intercept the pre-call initialization phase. First, drop a REST Proxy action right after the queue routing decision node and point it at PATCH /api/v2/telephony/users/softphones/{id}. Next, pass the exact turnServers payload in the request body and lock the transport parameter to tcp since UDP doesn’t play nice with corporate firewalls during the initial handshake. Then, map the 200 OK response to a local variable so the subsequent initiateCall action can read the updated session state. Sometimes the browser cache holds onto the old config for a minute or two. You’ll want to clear it manually if the widget still throws errors. The handshake usually clears up once the proxy finishes. Leave the retry count at two.