{"record":{"id":"c3fd1cace8c7c539","repo":"zeroclaw-labs/zeroclaw","slug":"twilio-call-failed-body","errorCode":null,"errorMessage":"Twilio call failed: {body}","messagePattern":"Twilio call failed: (.+?)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/voice_call.rs","lineNumber":160,"sourceCode":"                    self.api_base_url(),\n                    self.config.account_id\n                );\n                let resp = self\n                    .client\n                    .post(&url)\n                    .basic_auth(&self.config.account_id, Some(&self.config.auth_token))\n                    .form(&[\n                        (\"To\", to_number),\n                        (\"From\", &self.config.from_number),\n                        (\"StatusCallback\", &webhook_url),\n                        (\"Timeout\", &self.config.max_call_duration_secs.to_string()),\n                    ])\n                    .send()\n                    .await?;\n\n                if !resp.status().is_success() {\n                    let body = resp.text().await.unwrap_or_default();\n                    bail!(\"Twilio call failed: {body}\");\n                }\n\n                let json: serde_json::Value = serde_json::from_str(&resp.text().await?)?;\n                let call_sid = json[\"sid\"].as_str().unwrap_or(\"unknown\").to_string();\n                ::zeroclaw_log::record!(\n                    INFO,\n                    ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)\n                        .with_attrs(::serde_json::json!({\"call_sid\": call_sid, \"to\": to_number})),\n                    \"outbound call placed via Twilio\"\n                );\n                Ok(call_sid)\n            }\n            VoiceProvider::Telnyx => {\n                let url = format!(\"{}/calls\", self.api_base_url());\n                let resp = self\n                    .client\n                    .post(&url)\n                    .bearer_auth(&self.config.auth_token)","sourceCodeStart":142,"sourceCodeEnd":178,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/voice_call.rs#L142-L178","documentation":"Raised in the Twilio arm of VoiceCallChannel::execute_outbound_call when POST {api_base}/Accounts/{account_id}/Calls.json answers non-2xx. Authentication is HTTP Basic with account_id/auth_token from [channels.voice_call.<alias>]; the form body carries To, From, a StatusCallback webhook, and Timeout. place_call reaches this only when require_outbound_approval is off (otherwise it returns PENDING_APPROVAL). The raw Twilio error body (XML/JSON with a code and message) is included.","triggerScenarios":"401 for a wrong auth_token or account_id; Twilio error 21211 for a malformed To number; 21212/21214 when from_number is not a number purchased on that account; 21207 for unverified trial-account destinations; callback-URL errors when the configured webhook_base_url is not reachable over HTTP(S).","commonSituations":"The auth token was rotated in the Twilio console but not in config. From-numbers borrowed from another subaccount. Trial accounts calling unverified numbers. The webhook defaulting to http://localhost when webhook_base_url is unset, which Twilio rejects for status callbacks.","solutions":["Look up the numeric Twilio error code inside the body text; it names the exact violation.","For 401, correct account_id and auth_token in [channels.voice_call.<alias>].","Use E.164 for both numbers and ensure from_number is purchased/owned by that account.","On trial accounts, verify the destination number in the console first.","Set webhook_base_url to a publicly reachable HTTPS URL instead of the localhost default."],"exampleFix":"# before — From number not owned by this account; webhook defaults to localhost\n[channels.voice_call.desk]\naccount_id = \"ACxxxxxxxx\"\nauth_token = \"xxxx\"\nfrom_number = \"+15550009999\"\n\n# after\n[channels.voice_call.desk]\naccount_id = \"ACxxxxxxxx\"\nauth_token = \"xxxx\"\nfrom_number = \"+15550001111\"          # purchased on this account\nwebhook_base_url = \"https://bots.example.com\"","handlingStrategy":"try-catch","validationCode":"// E.164 check on both numbers before placing the call.\nfn is_e164(n: &str) -> bool {\n    let b = n.as_bytes();\n    b.len() >= 12 && b[0] == b'+' && b[1..].iter().all(|c| c.is_ascii_digit())\n}\n\nassert!(is_e164(to_number) && is_e164(&config.from_number));","typeGuard":"fn is_e164_number(n: &str) -> bool {\n    let b = n.as_bytes();\n    b.len() >= 12 && b[0] == b'+' && b[1..].iter().all(|c| c.is_ascii_digit())\n}","tryCatchPattern":"match channel.place_call(&to).await {\n    Ok(sid) if sid.starts_with(\"PENDING_APPROVAL:\") => Ok(sid), // approval flow\n    Ok(sid) => Ok(sid),\n    Err(err) if err.to_string().contains(\"Twilio call failed\") => {\n        // body embeds Twilio's numeric code: 401=auth, 21211=bad To,\n        // 21212/21214=From not owned, 21207=unverified trial destination.\n        // All are config/state errors — surface for fix, do not blind-retry.\n        Err(err.context(\"check account_id/auth_token, E.164 numbers, and From ownership\"))\n    }\n    Err(err) => Err(err),\n}","preventionTips":["Store account_id, auth_token, from_number together and rotate the token in the same change window as the Twilio console.","Use only numbers purchased on that Twilio account as from_number; format both numbers as E.164 (+countrycode...).","On trial accounts, verify destination numbers before calling; production accounts avoid 21207 entirely.","Set webhook_base_url to a public HTTPS endpoint — the localhost default makes status callbacks fail Twilio's validation."],"tags":["twilio","voice-call","http","telephony","auth"],"backgroundTag":"twilio-api-error","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}