{"record":{"id":"a3863227828da046","repo":"zeroclaw-labs/zeroclaw","slug":"plivo-call-failed-body","errorCode":null,"errorMessage":"Plivo call failed: {body}","messagePattern":"Plivo call failed: (.+?)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/voice_call.rs","lineNumber":229,"sourceCode":"                    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                    .json(&serde_json::json!({\n                        \"to\": to_number,\n                        \"from\": self.config.from_number,\n                        \"answer_url\": self.webhook_url(\"/voice/answer\"),\n                        \"hangup_url\": self.webhook_url(\"/voice/hangup\"),\n                        \"time_limit\": self.config.max_call_duration_secs,\n                    }))\n                    .send()\n                    .await?;\n\n                if !resp.status().is_success() {\n                    let body = resp.text().await.unwrap_or_default();\n                    bail!(\"Plivo call failed: {body}\");\n                }\n\n                let json: serde_json::Value = serde_json::from_str(&resp.text().await?)?;\n                let call_uuid = json[\"request_uuid\"]\n                    .as_str()\n                    .unwrap_or(\"unknown\")\n                    .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_uuid\": call_uuid, \"to\": to_number})),\n                    \"outbound call placed via Plivo\"\n                );\n                Ok(call_uuid)\n            }\n        }\n    }\n","sourceCodeStart":211,"sourceCodeEnd":247,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/voice_call.rs#L211-L247","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before — daemon behind no public host\n[channels.voice]\nmodel_provider = \"plivo\"\n# webhook_base_url unset -> answer_url becomes http://localhost:8080/voice/answer\n# Plivo: \"Plivo call failed: {\\\"error\\\":\\\"answer_url is not reachable\\\"}\"\n\n# after\n[channels.voice]\nmodel_provider = \"plivo\"\nwebhook_base_url = \"https://bot.example.com\"  # Plivo can GET /voice/answer and /voice/hangup","handlingStrategy":"try-catch","validationCode":"fn is_e164(n: &str) -> bool {\n    let digits: String = n.chars().filter(|c| c.is_ascii_digit()).collect();\n    n.starts_with('+') && (8..=15).contains(&digits.len())\n}\n\nif !is_e164(to_number) || !is_e164(&voice_config.from_number) {\n    anyhow::bail!(\"refusing to dial non-E.164 number: {to_number}\");\n}\nif voice_config.webhook_base_url.is_none() {\n    anyhow::bail!(\"channels.voice.webhook_base_url unset; Plivo answer_url will be localhost\");\n}","typeGuard":null,"tryCatchPattern":"match voice.place_call(to_number).await {\n    Ok(call_uuid) => { /* track uuid */ }\n    Err(e) if e.to_string().starts_with(\"Plivo call failed:\") => {\n        // Plivo error body names the cause (auth failed / number / answer_url);\n        // non-retryable until config or account state changes.\n    }\n    Err(e) => { /* transport errors may be transient: retry with backoff */ }\n}","preventionTips":["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."],"tags":["plivo","voice-call","http-api","config","webhook-url"],"backgroundTag":"voice-provider-api-error","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}