zeroclaw-labs/zeroclaw · error · anyhow::Error
Twilio call failed: {body}
Error message
Twilio call failed: {body} What it means
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.
Source
Thrown at crates/zeroclaw-channels/src/voice_call.rs:160
self.api_base_url(),
self.config.account_id
);
let resp = self
.client
.post(&url)
.basic_auth(&self.config.account_id, Some(&self.config.auth_token))
.form(&[
("To", to_number),
("From", &self.config.from_number),
("StatusCallback", &webhook_url),
("Timeout", &self.config.max_call_duration_secs.to_string()),
])
.send()
.await?;
if !resp.status().is_success() {
let body = resp.text().await.unwrap_or_default();
bail!("Twilio call failed: {body}");
}
let json: serde_json::Value = serde_json::from_str(&resp.text().await?)?;
let call_sid = json["sid"].as_str().unwrap_or("unknown").to_string();
::zeroclaw_log::record!(
INFO,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
.with_attrs(::serde_json::json!({"call_sid": call_sid, "to": to_number})),
"outbound call placed via Twilio"
);
Ok(call_sid)
}
VoiceProvider::Telnyx => {
let url = format!("{}/calls", self.api_base_url());
let resp = self
.client
.post(&url)
.bearer_auth(&self.config.auth_token)View on GitHub (pinned to 88bb9c8533)
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.
Example fix
# before — From number not owned by this account; webhook defaults to localhost [channels.voice_call.desk] account_id = "ACxxxxxxxx" auth_token = "xxxx" from_number = "+15550009999" # after [channels.voice_call.desk] account_id = "ACxxxxxxxx" auth_token = "xxxx" from_number = "+15550001111" # purchased on this account webhook_base_url = "https://bots.example.com"
Defensive patterns
Strategy: try-catch
Validate before calling
// E.164 check on both numbers before placing the call.
fn is_e164(n: &str) -> bool {
let b = n.as_bytes();
b.len() >= 12 && b[0] == b'+' && b[1..].iter().all(|c| c.is_ascii_digit())
}
assert!(is_e164(to_number) && is_e164(&config.from_number)); Type guard
fn is_e164_number(n: &str) -> bool {
let b = n.as_bytes();
b.len() >= 12 && b[0] == b'+' && b[1..].iter().all(|c| c.is_ascii_digit())
} Try / catch
match channel.place_call(&to).await {
Ok(sid) if sid.starts_with("PENDING_APPROVAL:") => Ok(sid), // approval flow
Ok(sid) => Ok(sid),
Err(err) if err.to_string().contains("Twilio call failed") => {
// body embeds Twilio's numeric code: 401=auth, 21211=bad To,
// 21212/21214=From not owned, 21207=unverified trial destination.
// All are config/state errors — surface for fix, do not blind-retry.
Err(err.context("check account_id/auth_token, E.164 numbers, and From ownership"))
}
Err(err) => Err(err),
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- ElevenLabs TTS API error ({}): {}
- Google TTS API error ({}): {}
- Twitter users/me failed ({status}): {err}
- Twitter DM send failed ({status}): {err}
- WhatsApp API error: {status}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/c3fd1cace8c7c539.
Report an issue: GitHub.