zeroclaw-labs/zeroclaw · error · anyhow::Error
Plivo call failed: {body}
Error message
Plivo call failed: {body} What it means
Raised by VoiceCallChannel::execute_outbound_call when the Plivo Call API returns non-2xx for POST {api_base_url}/Account/{account_id}/Call/. The request uses HTTP basic auth (account_id + auth_token) and a JSON body with to, from, answer_url, hangup_url, and time_limit; the response body is embedded in the error. It means Plivo refused to create the call — most often an auth, number, or answer_url problem called out in the body's 'error' field.
Source
Thrown at crates/zeroclaw-channels/src/voice_call.rs:229
self.config.account_id
);
let resp = self
.client
.post(&url)
.basic_auth(&self.config.account_id, Some(&self.config.auth_token))
.json(&serde_json::json!({
"to": to_number,
"from": self.config.from_number,
"answer_url": self.webhook_url("/voice/answer"),
"hangup_url": self.webhook_url("/voice/hangup"),
"time_limit": self.config.max_call_duration_secs,
}))
.send()
.await?;
if !resp.status().is_success() {
let body = resp.text().await.unwrap_or_default();
bail!("Plivo call failed: {body}");
}
let json: serde_json::Value = serde_json::from_str(&resp.text().await?)?;
let call_uuid = json["request_uuid"]
.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_uuid": call_uuid, "to": to_number})),
"outbound call placed via Plivo"
);
Ok(call_uuid)
}
}
}
View on GitHub (pinned to 88bb9c8533)
Solutions
- Read the embedded Plivo error body — it states 'authentication failed', 'not your number', or an invalid parameter directly.
- Confirm auth_id/auth_token pair is valid (test with curl -u AUTH_ID:AUTH_TOKEN https://api.plivo.com/v1/Account/{auth_id}/).
- Set channels.voice.webhook_base_url to a public https base; Plivo must be able to GET the generated /voice/answer URL.
- Make sure from_number is a Plivo-purchased number in E.164 and, on trial accounts, that the destination is a verified number.
- Verify your answer endpoint returns valid PlivoXML (Speak/Redirect) so the call does not fail after creation.
Example fix
# before — daemon behind no public host
[channels.voice]
model_provider = "plivo"
# webhook_base_url unset -> answer_url becomes http://localhost:8080/voice/answer
# Plivo: "Plivo call failed: {\"error\":\"answer_url is not reachable\"}"
# after
[channels.voice]
model_provider = "plivo"
webhook_base_url = "https://bot.example.com" # Plivo can GET /voice/answer and /voice/hangup 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())
}
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; Plivo answer_url will be localhost");
} Try / catch
match voice.place_call(to_number).await {
Ok(call_uuid) => { /* track uuid */ }
Err(e) if e.to_string().starts_with("Plivo call failed:") => {
// Plivo error body names the cause (auth failed / number / answer_url);
// non-retryable until config or account state changes.
}
Err(e) => { /* transport errors may be transient: retry with backoff */ }
} Prevention
- Keep auth_id/auth_token in a secret store and rotate both sides together.
- On trial accounts, only dial verified numbers; automate that check before dialing.
- Monitor the /voice/answer endpoint externally so an unreachable answer_url is caught before Plivo rejects calls.
- Return valid PlivoXML from answer/hangup endpoints; a reachable URL with wrong content still kills calls.
When it happens
Trigger: POST to Plivo /Account/{auth_id}/Call/ with (1) wrong auth_id/auth_token -> 401; (2) a trial account calling a number other than the verified sandbox number -> 400 'not allowed'; (3) from number not purchased on Plivo or not E.164 -> 400; (4) answer_url (built as {webhook_base_url}/voice/answer) unreachable or http://localhost:PORT because webhook_base_url is unset — Plivo validates answer_url reachability and rejects the request; (5) answer_url not returning valid PlivoXML at answer time.
Common situations: Using sandbox credentials in production (or vice versa); forgetting channels.voice.webhook_base_url so the channel advertises http://localhost:8080/voice/answer which Plivo cannot reach; trial accounts dialing unverified numbers; token rotated in the Plivo console but not in zeroclaw config.
Related errors
- Telnyx call failed: {body}
- unsupported room visibility '{other}': expected private or p
- channel '{}' does not support forge API requests
- createSession failed ({status}): {body}
- post failed ({status}): {body}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/a3863227828da046.
Report an issue: GitHub.