zeroclaw-labs/zeroclaw · error · anyhow::Error
Telnyx call failed: {body}
Error message
Telnyx call failed: {body} What it means
Raised by VoiceCallChannel::execute_outbound_call when the Telnyx Call Control API returns a non-2xx status for POST {api_base_url}/calls. The request authenticates with a bearer token (config.auth_token), sends connection_id (from config.account_id), to, from, webhook_url, and timeout_secs, and the response body is embedded verbatim in the error so the Telnyx error envelope (errors[0].code/detail) tells you the exact cause. It means Telnyx refused to create the call; the raw body is the authoritative diagnosis.
Source
Thrown at crates/zeroclaw-channels/src/voice_call.rs:191
VoiceProvider::Telnyx => {
let url = format!("{}/calls", self.api_base_url());
let resp = self
.client
.post(&url)
.bearer_auth(&self.config.auth_token)
.json(&serde_json::json!({
"connection_id": self.config.account_id,
"to": to_number,
"from": self.config.from_number,
"webhook_url": webhook_url,
"timeout_secs": self.config.max_call_duration_secs,
}))
.send()
.await?;
if !resp.status().is_success() {
let body = resp.text().await.unwrap_or_default();
bail!("Telnyx call failed: {body}");
}
let json: serde_json::Value = serde_json::from_str(&resp.text().await?)?;
let call_id = json["data"]["call_control_id"]
.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_id": call_id, "to": to_number})),
"outbound call placed via Telnyx"
);
Ok(call_id)
}
VoiceProvider::Plivo => {
let url = format!(
"{}/Account/{}/Call/",View on GitHub (pinned to 88bb9c8533)
Solutions
- Read the embedded Telnyx error body first — it names the failing field or token (e.g. "10111\" no object found for connection_id").
- Verify auth_token is a current Telnyx V2 API token and account_id is the Call Control application/connection id, not the account SID.
- Ensure from and to are E.164 (+15551234567, no spaces/dashes) and the from number is owned by the Telnyx project.
- Set channels.voice.webhook_base_url to a public https base so the generated webhook_url is reachable by Telnyx.
- Reproduce outside the agent with curl -X POST https://api.telnyx.com/v2/calls -H "Authorization: Bearer $TOKEN" to isolate config vs. code.
Example fix
# before — config.toml [channels.voice] model_provider = "telnyx" account_id = "SIM-xxxx" # wrong: account SID, not a Call Control connection id from_number = "+1 (555) 123-4567" # wrong: not E.164 # after [channels.voice] model_provider = "telnyx" account_id = "12345678-1234-1234-1234-123456789012" # Call Control Application/Connection id from_number = "+15551234567" # E.164 webhook_base_url = "https://bot.example.com" # public, not localhost
Defensive patterns
Strategy: try-catch
Validate before calling
fn is_e164(n: &str) -> bool {
let digits: String = n.chars().filter(|c| c.is_ascii_digit()).collect();
n.starts_with('+') && (8..=15).contains(&digits.len())
}
// before placing a Telnyx call
if !is_e164(to_number) || !is_e164(&voice_config.from_number) {
anyhow::bail!("refusing to dial non-E.164 number: {to_number}");
}
if voice_config.webhook_base_url.is_none() {
anyhow::bail!("channels.voice.webhook_base_url unset; Telnyx cannot reach status callbacks");
} Try / catch
match voice.place_call(to_number).await {
Ok(call_id) => { /* track id */ }
Err(e) if e.to_string().starts_with("Telnyx call failed:") => {
// e carries the raw Telnyx error body: log it, surface to operator,
// and treat as non-retryable (4xx auth/validation) until config changes.
}
Err(e) => { /* transport errors (reqwest) may be transient: safe to retry */ }
} Prevention
- Validate E.164 format for both from_number and destination before calling place_call.
- Smoke-test credentials at boot with a cheap authenticated GET so a rotated token fails fast with a clear message.
- Always set channels.voice.webhook_base_url in any non-local deployment.
- Log and alert on the full error string — the embedded Telnyx body is the actionable diagnosis.
When it happens
Trigger: POST https://api.telnyx.com/v2/calls with (1) an expired/rotated V2 API token -> 401; (2) a connection_id that is not a valid Call Control Application ID (e.g. an account SID pasted instead) -> 422 'no object found for connection_id'; (3) from/to numbers not in E.164 (spaces, dashes, missing +) -> 422; (4) a from number not purchased/rented in the Telnyx project; (5) require_outbound_approval=false and any of the above fields wrong. Note webhook_url defaults to http://localhost:{webhook_port} when channels.voice.webhook_base_url is unset, which Telnyx will reject as unreachable.
Common situations: Rotating the Telnyx API key but not updating zeroclaw config; pasting the Telnyx Account ID into account_id (which must hold the Call Control connection/application id); copying a phone number from a contact card with formatting characters; running the daemon locally without webhook_base_url so Telnyx cannot reach status callbacks.
Related errors
- Plivo call failed: {body}
- Destination {} is not in allowed list
- unsupported room visibility '{other}': expected private or p
- channel '{}' does not support forge API requests
- createSession failed ({status}): {body}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/c708d426853a7e7e.
Report an issue: GitHub.