The full path
Five hops on the way in. Audio flowing both directions. Latency under 200ms end to end.Why this took three tries
Before settling on Asterisk, we tried three other paths. Each one failed for a specific, documented reason. Worth understanding the failure tree to appreciate why Asterisk works. What the failures taught us:- Twilio’s rejection told us Meta’s SDP is non-standard at the edges. Whatever bridges Meta’s audio has to be tolerant of WebRTC-style SDP variants.
- Voice runtime direct failed on media setup, not signalling. Meta’s Opus codec parameters were the specific blocker —
fmtplines that the destination couldn’t parse. The fix had to involve transcoding away from Opus. - FreeSWITCH’s packaging gate was a deployment concern, not a technical one. We’d have lost a day fighting a paid account requirement that didn’t unblock anything else.
- Asterisk worked first try once we got the codec story right. Ubuntu’s universe repo includes Asterisk. The Opus codec module is a free Sangoma binary. The configuration involves three files and three concepts.
The Asterisk configuration
Three configuration files do the work.1. pjsip.conf — transport, endpoints, identifiers
The transport block opens the TLS listener:
[meta] endpoint by source IP. Meta publishes their SIP origin IP ranges; we encode them here.
2. extensions.conf — the dialplan
The dialplan is what actually translates the call into a voice-runtime-bound stream.
Set(CHANNEL(audionativeformat)=ulaw) is the key line. It forces the channel format to G.711 PCMU before bridging — that’s the transcode step.
The Dial(PJSIP/${EXTEN}@voice-runtime) line bridges the call to the voice runtime endpoint, which is a separate pjsip.conf endpoint pointing at the voice runtime’s SIP ingress.
3. asterisk.conf and modules.conf — boot
Standard Asterisk configuration. Module load order matters because the Opus codec module has to load before any endpoint that references it. We pin the load order to avoid a race at boot.
Why PCMU on the voice-runtime leg
We don’t pass Meta’s Opus straight through to the voice runtime. We transcode to PCMU. Why: the voice runtime accepts Opus in principle, but Meta’s specific Opus parameter set (fmtp lines, particularly the usedtx, useinbandfec, maxplaybackrate combinations) trips the runtime’s media negotiation. Symptom: transport-never-connected on the media plane. The setup signalling completes; no audio flows.
PCMU is older, less efficient, and universally supported. By transcoding at Asterisk, we drop Meta’s tricky Opus fmtp parameters entirely and present a clean G.711 stream to the voice runtime. Audio quality is acceptable (it’s a voice call, not music). Latency added by transcoding is under 20ms on commodity hardware.
For Asterisk, this is a one-line dialplan instruction. For us, it’s the difference between a call connecting and a call silently failing.
SDES-SRTP
The leg from Asterisk to the voice runtime uses SDES (Session Description Protocol Security Descriptions) for SRTP key exchange. SDES sends the encryption keys in the SIP SDP body, protected by the TLS-transported signalling. This is the simpler of SRTP’s two key-exchange modes (the other being DTLS-SRTP). Voice runtime accepts SDES; Asterisk supports it natively. The signalling channel is already TLS so the key exchange is protected. The media itself is encrypted (G.711 inside SRTP), so the audio is private. We don’t use DTLS-SRTP because the runtime’s DTLS handshake had compatibility issues with Asterisk’s. SDES works first try.The voice runtime side
When Asterisk’sDial(PJSIP/${EXTEN}@voice-runtime) fires, the voice runtime sees a fresh inbound SIP call. From its perspective:
- Source: our Asterisk instance.
- Codec: G.711 PCMU.
- Media: SDES-SRTP.
- Caller ID: passed through from the WhatsApp Calling caller (the user’s phone number).
Operational concerns
Asterisk uptime
Asterisk is a long-running daemon. Our systemd unit restarts on failure. Health check:asterisk -rx "core show channels" should not error. We monitor for unexpected restarts; a clean week is the baseline.
Certificate management
guild.crt and guild.key in /etc/asterisk/keys/ are pinned. Cert renewal is handled at the same cadence as nginx’s. The Asterisk daemon reloads its TLS config on SIGHUP without dropping calls.
Meta IP rotation
Meta’s SIP origin IPs change occasionally. We monitor foridentify failures (calls that don’t match any known endpoint) and update the identifier table. A small operational task; happens roughly quarterly.
Codec module updates
The Sangoma Opus codec module updates infrequently. We track upstream and update at quiet hours.Scaling
A single Asterisk instance comfortably handles hundreds of concurrent calls on commodity hardware. Our daily-active-call count is far below that ceiling. At 10K-concurrent-call scale, we’d cluster Asterisk with a SIP load balancer in front; the configuration is well-understood and documented.What this gives the product
WhatsApp Calling lands directly on Tola. A Job Seeker doesn’t need to remember a Guild phone number. The Guild contact is already in their WhatsApp. They tap the call button — the same call button they’d use to call a friend — and Tola answers. The whole protocol stack below is invisible. For onboarding a Job Seeker who’s never heard of Guild before, this is the smoothest possible path. “Send a message to this contact. Or tap the call button.” The user doesn’t have to download anything, dial anything, type anything. For most platforms, this UX requires either a custom app (with a custom calling stack) or a separate phone number. We have neither requirement, and the platform is just as accessible.What to read next
SMS infrastructure
The other custom rail — Go gateway and paired Android devices.
Channels → Voice
The product-level view of Tola.
Channels → WhatsApp Calling
The user-facing page about tap-to-call.