zeroclaw-labs/zeroclaw · error

ElevenLabs TTS API error ({}): {}

Error message

ElevenLabs TTS API error ({}): {}

What it means

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.

Source

Thrown at crates/zeroclaw-channels/src/tts.rs:260

            .client
            .post(&url)
            .header("xi-api-key", &self.api_key)
            .json(&body)
            .send()
            .await
            .context("Failed to send ElevenLabs TTS request")?;

        let status = resp.status();
        if !status.is_success() {
            let error_body: serde_json::Value = resp
                .json()
                .await
                .unwrap_or_else(|_| serde_json::json!({"error": "unknown"}));
            let msg = error_body["detail"]["message"]
                .as_str()
                .or_else(|| error_body["detail"].as_str())
                .unwrap_or("unknown error");
            bail!("ElevenLabs TTS API error ({}): {}", status, msg);
        }

        let bytes = resp
            .bytes()
            .await
            .context("Failed to read ElevenLabs TTS response body")?;
        Ok(bytes.to_vec())
    }

    fn supported_voices(&self) -> Vec<String> {
        // ElevenLabs voices are user-specific; return empty (dynamic lookup).
        Vec::new()
    }

    fn supported_formats(&self) -> Vec<String> {
        ["mp3", "pcm", "ulaw"]
            .iter()
            .map(|s| (*s).to_string())

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. 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.
  2. For voice-not-found, confirm the voice ID exists on that account via GET /v1/voices and fix [providers.tts.elevenlabs.<alias>].voice.
  3. 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.
  4. Retry once with backoff on transient 5xx or rate-limit responses.

Example fix

# before — env var no longer read since V0.8.0
# export ELEVENLABS_API_KEY=...

# after — key lives in config (or ZEROCLAW_providers__tts__elevenlabs__main__api_key)
[providers.tts.elevenlabs.main]
api_key = "xi-..."
model = "eleven_multilingual_v2"
Defensive patterns

Strategy: retry

Validate before calling

// Optional pre-flight: confirm the voice ID belongs to this account.
async fn elevenlabs_voice_exists(client: &reqwest::Client, key: &str, voice_id: &str) -> bool {
    client
        .get(format!("https://api.elevenlabs.io/v1/voices/{voice_id}"))
        .header("xi-api-key", key)
        .send()
        .await
        .map(|r| r.status().is_success())
        .unwrap_or(false)
}

Try / catch

let out = mgr.synthesize(text).await;
if let Err(ref err) = out {
    let msg = err.to_string();
    if msg.contains("ElevenLabs TTS API error (401)") {
        // quota exhausted or bad key: stop retrying, surface for credential fix
    } else if msg.contains("(422)") {
        // model_id / voice_settings invalid: fix config, do not retry
    } else if msg.contains("(42") || msg.contains("(5") {
        // rate limit / transient: retry with backoff
        tokio::time::sleep(Duration::from_secs(3)).await;
        return mgr.synthesize(text).await;
    }
}
out

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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