Fixes for setup, webhook exposure, credentials, signature verification, Google Meet dial-in, and silent realtime calls. Part of the Voice call plugin guide.

Troubleshooting

Call placement fails to save its initial record

Voice Call reserves pending capacity while saving the initial record, then publishes the active call and contacts the carrier only after the write succeeds. If the write fails, placement reports the storage error without dialing and releases the reservation. Restore access to the state directory, then retry; a failed placement does not consume maxConcurrentCalls capacity.

Webhook acknowledgments and transcript-driven replies wait for call-record persistence. Token-bound realtime streams wait for pending call updates before matching the carrier ID. Failed event writes remain retryable, and shutdown drains admitted call work after closing webhook and stream producers.

If another realtime stream becomes active during admission, a failure to create the new bridge leaves that active call connected.

Setup fails webhook exposure

Run setup from the same environment that runs the Gateway:

apus voicecall setup
apus voicecall setup --json

For twilio, telnyx, and plivo, webhook-exposure must be green. A configured publicUrl still fails when it points at local or private network space, because the carrier cannot call back into those addresses. Do not use localhost, 127.0.0.1, 0.0.0.0, 10.x, 172.16.x-172.31.x, 192.168.x, 169.254.x, fc00::/7, fd00::/8, or other carrier-grade-NAT ranges as publicUrl.

Twilio notify-mode outbound calls send their initial <Say> TwiML directly in the create-call request, so the first spoken message does not depend on Twilio fetching webhook TwiML. A public webhook is still required for status callbacks, conversation calls, pre-connect DTMF, realtime streams, and post-connect call control.

Use one public exposure path:

{
  plugins: {
    entries: {
      "voice-call": {
        config: {
          publicUrl: "https://voice.example.com/voice/webhook",
          // or
          tunnel: { provider: "ngrok" },
          // or
          tailscale: { mode: "funnel", port: 8443, path: "/voice/webhook" },
        },
      },
    },
  },
}

Config changes apply automatically with the default hybrid reload mode (see Hot reload). After application, run:

apus voicecall setup
apus voicecall smoke

voicecall smoke is a dry run unless you pass --yes.

Provider credentials fail

Check the selected provider and the required credential fields:

Credentials must exist on the Gateway host. Editing a local shell profile does not change the running Gateway's environment. Update its service environment and restart the Gateway when changing environment-based credentials.

Calls start but provider webhooks do not arrive

Confirm the provider console points at the exact public webhook URL:

https://voice.example.com/voice/webhook

For a Twilio inbound number, configure both number-level callbacks in the Twilio Console:

Media Streams stop/WebSocket close handling is the primary auto-end path and does not depend on the HTTP status callback. Twilio's optional &lt;Stream statusCallback&gt; is a separate stream-diagnostic signal and is not required for teardown. apus voicecall setup validates local configuration and webhook exposure; it cannot inspect or change Twilio Console settings.

Then inspect runtime state:

apus voicecall status --call-id <id>
apus voicecall tail
apus logs --follow

Common causes:

When a reverse proxy or tunnel is in front of the Gateway, set webhookSecurity.allowedHosts to the public hostname, or use webhookSecurity.trustedProxyIPs for a known proxy address. Use webhookSecurity.trustForwardingHeaders only when the proxy boundary is under your control.

Signature verification fails

Twilio and Plivo URL signatures use publicUrl when it is configured: its scheme, host, and path are preserved, while the request query is applied. Without publicUrl, Apus reconstructs the URL from the request. Telnyx signatures do not include the request URL. If signatures fail:

Google Meet Twilio joins fail

Google Meet uses this plugin for Twilio dial-in joins. First verify Voice Call:

apus voicecall setup
apus voicecall smoke --to "+15555550123"

Then verify the Google Meet transport explicitly:

apus googlemeet setup --transport twilio

If Voice Call is green but the Meet participant never joins, check the Meet dial-in number, PIN, and --dtmf-sequence. The phone call can be healthy while the meeting rejects or ignores an incorrect DTMF sequence.

Google Meet starts the Twilio phone leg through voicecall.start with a pre-connect DTMF sequence. PIN-derived sequences include the Google Meet plugin's voiceCall.dtmfDelayMs (default 12000 ms) as leading Twilio wait digits, because Meet dial-in prompts can arrive late. Voice Call then redirects back to realtime handling before the intro greeting is requested.

Use apus logs --follow for the live phase trace. A healthy Twilio Meet join logs this order:

apus voicecall tail still shows persisted call records; useful for call state and transcripts, but not every webhook/realtime transition appears there.

Realtime call has no speech

Confirm only one audio mode is enabled: realtime.enabled and streaming.enabled cannot both be true.

For realtime Twilio/Telnyx calls, also verify: