← back to Butlr
twilio: fix StatusCallbackEvent — repeated param not space-string
fc2ef8e5d4eb07cee83cd4d370f5702aabf77fa8 · 2026-05-12 17:28:35 -0700 · SteveStudio2
Twilio's REST API rejects 'initiated ringing answered completed' as
a single string with error 21626 (Invalid Events) and silently falls
back to firing only the 'completed' webhook. This is exactly why our
/twilio/status/:id route never received ringing/answered events all
day. Fix: use URLSearchParams.append() to pass StatusCallbackEvent
as a repeated param.
Found by checking Monitor v1 Alerts API on the 23:45 UTC calls today.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
Files touched
Diff
commit fc2ef8e5d4eb07cee83cd4d370f5702aabf77fa8
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Tue May 12 17:28:35 2026 -0700
twilio: fix StatusCallbackEvent — repeated param not space-string
Twilio's REST API rejects 'initiated ringing answered completed' as
a single string with error 21626 (Invalid Events) and silently falls
back to firing only the 'completed' webhook. This is exactly why our
/twilio/status/:id route never received ringing/answered events all
day. Fix: use URLSearchParams.append() to pass StatusCallbackEvent
as a repeated param.
Found by checking Monitor v1 Alerts API on the 23:45 UTC calls today.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
---
lib/twilio.js | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/lib/twilio.js b/lib/twilio.js
index 78d841e..c6b7b43 100644
--- a/lib/twilio.js
+++ b/lib/twilio.js
@@ -160,18 +160,26 @@ async function dialCall(call) {
log('LIVE mode requested but Twilio creds missing:', auth.error || 'no_phone_number_or_public_url');
return { ok: false, error: 'twilio_creds_missing', detail: auth.error };
}
+ // Twilio requires StatusCallbackEvent to appear as a REPEATED param
+ // (StatusCallbackEvent=initiated&StatusCallbackEvent=ringing&…), NOT
+ // a single space-separated string. Passing a string makes Twilio reject
+ // ALL events except the implicit `completed`, which is why our
+ // /twilio/status/:id route never fires for ringing/answered/etc. Fix:
+ // use URLSearchParams.append() per value.
const body = new URLSearchParams({
To: call.business_phone,
From: fromNum,
Url: `${publicUrl}/twilio/twiml/${call.id}`,
StatusCallback: `${publicUrl}/twilio/status/${call.id}`,
StatusCallbackMethod: 'POST',
- StatusCallbackEvent: 'initiated ringing answered completed',
MachineDetection: 'DetectMessageEnd',
AsyncAmd: 'true',
AsyncAmdStatusCallback: `${publicUrl}/twilio/amd/${call.id}`,
AsyncAmdStatusCallbackMethod: 'POST',
});
+ for (const evt of ['initiated', 'ringing', 'answered', 'completed']) {
+ body.append('StatusCallbackEvent', evt);
+ }
try {
const resp = await fetch(`https://api.twilio.com/2010-04-01/Accounts/${auth.accountSid}/Calls.json`, {
method: 'POST',
← bc26177 deploy.conf: lock in data/ excludes — rsync had wiped real c
·
back to Butlr
·
audio: finalize-recordings.js + diagnostics that found Twili 4a77606 →