decolua/9router · error · Error

ElevenLabs TTS failed: ${res.status}

Error message

ElevenLabs TTS failed: ${res.status}

What it means

The ElevenLabs synthesize call POSTs to /v1/text-to-speech/{voiceId}; on a non-OK response it first tries to parse the JSON body and surface ElevenLabs' own `detail.message`, falling back to this generic `ElevenLabs TTS failed: <status>` when the body is not parseable or lacks a message. It means ElevenLabs rejected the synthesis request or the request failed upstream.

Source

Thrown at open-sse/handlers/ttsProviders/elevenlabs.js:42

export default {
  async synthesize(text, model, credentials) {
    if (!credentials?.apiKey) throw new Error("ElevenLabs API key required");
    let modelId = "eleven_flash_v2_5";
    let voiceId = model;
    if (model && model.includes("/")) [modelId, voiceId] = model.split("/");

    const res = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
      method: "POST",
      headers: { "xi-api-key": credentials.apiKey, "Content-Type": "application/json" },
      body: JSON.stringify({
        text,
        model_id: modelId,
        voice_settings: { stability: 0.5, similarity_boost: 0.75 },
      }),
    });
    if (!res.ok) {
      const err = await res.json().catch(() => ({}));
      throw new Error(err?.detail?.message || `ElevenLabs TTS failed: ${res.status}`);
    }
    const buf = await res.arrayBuffer();
    if (buf.byteLength < 1024) throw new Error("ElevenLabs TTS returned empty audio");
    return { base64: Buffer.from(buf).toString("base64"), format: "mp3" };
  },
};

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. If the message shown is ElevenLabs' own detail.message, follow it; if you only got the status, re-run with the raw response logged to capture the body.
  2. 401 → regenerate/update the API key; 429/quota → wait or upgrade the ElevenLabs subscription.
  3. Verify the voice id exists: GET /v1/voices with the same key and use one of its voice_ids (model string is `modelId/voiceId`).
  4. Confirm the account has access to the requested model_id; switch to `eleven_multilingual_v2` or `eleven_flash_v2_5` per your plan.
  5. If the response body is HTML (proxy error), check your network path/proxy rather than ElevenLabs itself.

Example fix

// before
await synthesize(text, "eleven_flash_v2_5/old-voice-id", { apiKey });
// after: verify the voice exists first
const list = await fetchElevenLabsVoices(apiKey);
const voice = list.find(v => v.voice_id === voiceId || v.name === voiceId);
if (!voice) throw new Error(`Unknown ElevenLabs voice: ${voiceId}`);
await synthesize(text, `eleven_flash_v2_5/${voice.voice_id}`, { apiKey });
Defensive patterns

Strategy: retry

Validate before calling

const voices = await fetchElevenLabsVoices(apiKey); // throws early on bad key
const voice = voices.find(v => v.voice_id === voiceId || v.name === voiceId);
if (!voice) throw new Error(`Unknown ElevenLabs voice: ${voiceId}`);
if (!text?.trim()) throw new Error("TTS text is empty");

Try / catch

try {
  const audio = await elevenlabs.synthesize(text, `${modelId}/${voiceId}`, { apiKey });
} catch (e) {
  if (/failed: 429/.test(e.message)) {
    await sleep(5000);
    return elevenlabs.synthesize(text, `${modelId}/${voiceId}`, { apiKey });
  }
  if (/failed: 40[13]/.test(e.message)) throw new Error("Check ElevenLabs key/plan access for this model");
  throw e;
}

Prevention

When it happens

Trigger: Non-OK response from POST https://api.elevenlabs.io/v1/text-to-speech/{voiceId}: unknown/removed voice_id (404? actually 400/422), invalid key (401), quota exceeded or free-tier limit (401/429), model_id not available on the account's plan (e.g. eleven_flash_v2_5 unavailable), malformed body, or a non-JSON error body (proxy/CDN error page) so the detail parse fails.

Common situations: Voice id deleted or renamed in the ElevenLabs account; using a premium model id on a free plan; running out of character quota; sending text with invalid characters; a reverse proxy returning an HTML 502 that defeats the JSON detail extraction, leaving only the status code.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/0b0368ceb3cef34a. Report an issue: GitHub.