zeroclaw-labs/zeroclaw · error

ElevenLabs voice ID contains invalid characters: {voice}

Error message

ElevenLabs voice ID contains invalid characters: {voice}

What it means

ElevenLabsTtsProvider::synthesize interpolates the voice string directly into the URL path (https://api.elevenlabs.io/v1/text-to-speech/{voice}). Before sending, it requires every character of the voice to be ASCII alphanumeric, '-' or '_'; anything else bails immediately. This is a path-injection guard that stops '/', '?', '#', '.', and whitespace from rewriting the request URL.

Source

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

}

#[async_trait::async_trait]
impl TtsProvider for ElevenLabsTtsProvider {
    fn name(&self) -> &str {
        "elevenlabs"
    }

    fn output_format(&self) -> &str {
        // ElevenLabs default output is MP3 (mp3_44100_128).
        "mp3"
    }

    async fn synthesize(&self, text: &str, voice: &str) -> Result<Vec<u8>> {
        if !voice
            .chars()
            .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
        {
            bail!("ElevenLabs voice ID contains invalid characters: {voice}");
        }
        let url = format!("https://api.elevenlabs.io/v1/text-to-speech/{voice}");
        let body = serde_json::json!({
            "text": text,
            "model_id": self.model_id,
            "voice_settings": {
                "stability": self.stability,
                "similarity_boost": self.similarity_boost,
            },
        });

        let resp = self
            .client
            .post(&url)
            .header("xi-api-key", &self.api_key)
            .json(&body)
            .send()
            .await

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Use the ElevenLabs voice ID (alphanumeric, '-' or '_'), not the display name.
  2. Set [providers.tts.elevenlabs.<alias>].voice to that ID so TtsManager picks it automatically.
  3. Trim whitespace from the configured or passed voice before calling synthesize.
  4. List your account's real voice IDs with GET https://api.elevenlabs.io/v1/voices and copy the id field.

Example fix

# before — display name, contains a space
[providers.tts.elevenlabs.main]
api_key = "xi-..."
voice = "Rachel"

# after — the account's voice ID
[providers.tts.elevenlabs.main]
api_key = "xi-..."
voice = "21m00Tcm4TlvDq8ikWAM"
Defensive patterns

Strategy: validation

Validate before calling

// Run before calling synthesize/synthesize_with_voice.
fn is_valid_elevenlabs_voice_id(voice: &str) -> bool {
    !voice.is_empty()
        && voice.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
}

assert!(is_valid_elevenlabs_voice_id(&voice), "pass the ElevenLabs voice ID, not the name");

Type guard

fn is_elevenlabs_voice_id(v: &str) -> bool {
    v.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_') && !v.is_empty()
}

Prevention

When it happens

Trigger: Calling synthesize/synthesize_with_voice with an ElevenLabs voice name ("Rachel", "Aria"), a voice label containing a space or dot, a URL-encoded voice id, or any value with '/', '\', or ':' in it. Config values like voice = "Rachel (English)" under [providers.tts.elevenlabs.<alias>] hit the same check.

Common situations: Developers copy the human-readable voice name from the ElevenLabs dashboard instead of the 20-char voice ID (for example 21m00Tcm4TlvDq8ikWAM). A stray space or newline from copy-paste into TOML also trips it.

Understand the failure class

Related errors


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