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

  1. Read the embedded Plivo error body — it states 'authentication failed', 'not your number', or an invalid parameter directly.
  2. Confirm auth_id/auth_token pair is valid (test with curl -u AUTH_ID:AUTH_TOKEN https://api.plivo.com/v1/Account/{auth_id}/).
  3. Set channels.voice.webhook_base_url to a public https base; Plivo must be able to GET the generated /voice/answer URL.
  4. Make sure from_number is a Plivo-purchased number in E.164 and, on trial accounts, that the destination is a verified number.
  5. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/a3863227828da046. Report an issue: GitHub.