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()
.awaitView on GitHub (pinned to 88bb9c8533)
Solutions
- Use the ElevenLabs voice ID (alphanumeric, '-' or '_'), not the display name.
- Set [providers.tts.elevenlabs.<alias>].voice to that ID so TtsManager picks it automatically.
- Trim whitespace from the configured or passed voice before calling synthesize.
- 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
- Store voice IDs (20-char alphanumerics) in config, never display names.
- Fetch the canonical id list from GET /v1/voices at setup time and copy the id field verbatim.
- Trim configured voice values in TOML to avoid trailing-space surprises.
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- ElevenLabs TTS API error ({}): {}
- Edge TTS binary_path must be a bare command name without pat
- Edge TTS binary_path must be one of {:?}, got: {raw_path}
- TTS text must not be empty
- TTS text too long ({} chars, max {})
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/99e0bca37516fe84.
Report an issue: GitHub.