Skip to main content
Here’s a fact most platforms don’t have: when a Guild user opens their WhatsApp chat with us and taps the call button, they’re talking to Tola in real time. Not a callback. Not a redirect to a separate phone number. The voice agent answers the WhatsApp call directly. This works because of a sibling Asterisk process on the same VPS, listening on TLS 5061, doing protocol translation between Meta’s WebRTC-flavoured SIP and the format our voice runtime accepts. This page is how the bridge works, and why it took us three failed attempts to get there.

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:
  1. 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.
  2. Voice runtime direct failed on media setup, not signalling. Meta’s Opus codec parameters were the specific blocker — fmtp lines that the destination couldn’t parse. The fix had to involve transcoding away from Opus.
  3. 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.
  4. 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:
An endpoint block defines what we’ll accept from Meta:
An identifier block resolves Meta calls to the [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’s Dial(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).
The runtime picks up. Tola greets in Nigerian English. Subsequent flow is identical to a PSTN call — caller resolver, tool dispatcher, conversation state, all the things detailed in Services → voice-svc. The user has no idea any of this happened. They tapped Call in WhatsApp; Tola answered.

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 for identify 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.

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.