{"record":{"id":"08a6a7c4d212b085","repo":"zeroclaw-labs/zeroclaw","slug":"elevenlabs-tts-api-error","errorCode":null,"errorMessage":"ElevenLabs TTS API error ({}): {}","messagePattern":"ElevenLabs TTS API error \\((.+?)\\): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/tts.rs","lineNumber":260,"sourceCode":"            .client\n            .post(&url)\n            .header(\"xi-api-key\", &self.api_key)\n            .json(&body)\n            .send()\n            .await\n            .context(\"Failed to send ElevenLabs TTS request\")?;\n\n        let status = resp.status();\n        if !status.is_success() {\n            let error_body: serde_json::Value = resp\n                .json()\n                .await\n                .unwrap_or_else(|_| serde_json::json!({\"error\": \"unknown\"}));\n            let msg = error_body[\"detail\"][\"message\"]\n                .as_str()\n                .or_else(|| error_body[\"detail\"].as_str())\n                .unwrap_or(\"unknown error\");\n            bail!(\"ElevenLabs TTS API error ({}): {}\", status, msg);\n        }\n\n        let bytes = resp\n            .bytes()\n            .await\n            .context(\"Failed to read ElevenLabs TTS response body\")?;\n        Ok(bytes.to_vec())\n    }\n\n    fn supported_voices(&self) -> Vec<String> {\n        // ElevenLabs voices are user-specific; return empty (dynamic lookup).\n        Vec::new()\n    }\n\n    fn supported_formats(&self) -> Vec<String> {\n        [\"mp3\", \"pcm\", \"ulaw\"]\n            .iter()\n            .map(|s| (*s).to_string())","sourceCodeStart":242,"sourceCodeEnd":278,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/tts.rs#L242-L278","documentation":"Raised by ElevenLabsTtsProvider::synthesize when POST /v1/text-to-speech/{voice_id} returns non-2xx. Authentication uses the xi-api-key header from [providers.tts.elevenlabs.<alias>].api_key. The message extracts detail.message (or a string detail) from the ElevenLabs error body; FastAPI-style validation errors surface there as \"unknown error\" when the shape differs.","triggerScenarios":"401 with an invalid or free-tier-exhausted key, 422 when voice_settings (stability/similarity_boost) or model_id (default eleven_monolingual_v1) is invalid, 404/401-style rejection when the voice ID does not belong to the account, and rate-limit rejections on the starter plan.","commonSituations":"The ELEVENLABS_API_KEY env fallback was eradicated in V0.8.0, so upgraded deployments silently lose the key and send an empty or placeholder value. Free-tier keys hit the character quota mid-run. A model_id deprecated by ElevenLabs (old multilingual v1 names) starts failing after upstream changes.","solutions":["For 401/quota messages, verify the key at [providers.tts.elevenlabs.<alias>].api_key (env grammar ZEROCLAW_providers__tts__elevenlabs__<alias>__api_key) and check the subscription usage page.","For voice-not-found, confirm the voice ID exists on that account via GET /v1/voices and fix [providers.tts.elevenlabs.<alias>].voice.","For 422, set model_id to a current model (eleven_multilingual_v2, eleven_turbo_v2_5) and keep stability/similarity_boost in 0.0..=1.0.","Retry once with backoff on transient 5xx or rate-limit responses."],"exampleFix":"# before — env var no longer read since V0.8.0\n# export ELEVENLABS_API_KEY=...\n\n# after — key lives in config (or ZEROCLAW_providers__tts__elevenlabs__main__api_key)\n[providers.tts.elevenlabs.main]\napi_key = \"xi-...\"\nmodel = \"eleven_multilingual_v2\"","handlingStrategy":"retry","validationCode":"// Optional pre-flight: confirm the voice ID belongs to this account.\nasync fn elevenlabs_voice_exists(client: &reqwest::Client, key: &str, voice_id: &str) -> bool {\n    client\n        .get(format!(\"https://api.elevenlabs.io/v1/voices/{voice_id}\"))\n        .header(\"xi-api-key\", key)\n        .send()\n        .await\n        .map(|r| r.status().is_success())\n        .unwrap_or(false)\n}","typeGuard":null,"tryCatchPattern":"let out = mgr.synthesize(text).await;\nif let Err(ref err) = out {\n    let msg = err.to_string();\n    if msg.contains(\"ElevenLabs TTS API error (401)\") {\n        // quota exhausted or bad key: stop retrying, surface for credential fix\n    } else if msg.contains(\"(422)\") {\n        // model_id / voice_settings invalid: fix config, do not retry\n    } else if msg.contains(\"(42\") || msg.contains(\"(5\") {\n        // rate limit / transient: retry with backoff\n        tokio::time::sleep(Duration::from_secs(3)).await;\n        return mgr.synthesize(text).await;\n    }\n}\nout","preventionTips":["Since V0.8.0 the key must live in [providers.tts.elevenlabs.<alias>].api_key; drop ELEVENLABS_API_KEY from deployment scripts.","Track free-tier character quota in your own metrics; ElevenLabs returns 401 on exhaustion, which looks like a bad key.","Pin model to a currently documented model_id; check the ElevenLabs changelog after upgrades."],"tags":["elevenlabs","tts","http","api","auth"],"backgroundTag":"elevenlabs-api-error","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}