sipeed/picoclaw · error
ElevenLabs API error (status %d): %s
Error message
ElevenLabs API error (status %d): %s
What it means
Thrown when the ElevenLabs speech-to-text endpoint answers with any status other than 200. The message embeds the numeric status and the raw response body, so the upstream reason travels with the error. Common mappings: 401 invalid/missing Xi-Api-Key, 422 validation (unsupported audio format, file too large, wrong model_id), 429 rate limit or concurrency cap, 5xx upstream failure. Note this error is built with %d/%s, not %w — the status code is not programmatically unwrappable by callers.
Source
Thrown at pkg/audio/asr/elevenlabs_transcriber.go:125
resp, err := t.httpClient.Do(req)
if err != nil {
logger.ErrorCF("voice", "Failed to send request", map[string]any{"error": err})
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
logger.ErrorCF("voice", "Failed to read response", map[string]any{"error": err})
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
logger.ErrorCF("voice", "ElevenLabs API error", map[string]any{
"status_code": resp.StatusCode,
"response": string(body),
})
return nil, fmt.Errorf("ElevenLabs API error (status %d): %s", resp.StatusCode, string(body))
}
logger.DebugCF("voice", "Received response from ElevenLabs API", map[string]any{
"status_code": resp.StatusCode,
"response_size_bytes": len(body),
})
var result TranscriptionResponse
if err := json.Unmarshal(body, &result); err != nil {
logger.ErrorCF("voice", "Failed to unmarshal response", map[string]any{"error": err})
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
}
logger.InfoCF("voice", "ElevenLabs transcription completed successfully", map[string]any{
"text_length": len(result.Text),
"language": result.Language,
"transcription_preview": utils.Truncate(result.Text, 50),
})View on GitHub (pinned to 49183d7e8d)
Solutions
- Read the embedded body in the error string — it states the exact violation (e.g. 'file_mime_type is not supported').
- 401: verify the XI-Api-Key value and that the key is active in the ElevenLabs dashboard.
- 422: convert the audio to a supported format (16kHz+ WAV/MP3/Opus) and check duration/size caps before upload.
- 429: back off (Retry-After header) and serialize or throttle transcription jobs to the plan's concurrency.
- 5xx: retry with exponential backoff; check status.elevenlabs.io.
- Improve diagnosability: wrap with %w and truncate the body instead of inlining it fully.
Example fix
// before (library code)
return nil, fmt.Errorf("ElevenLabs API error (status %d): %s", resp.StatusCode, string(body))
// after: keep it unwrappable and bounded
type APIError struct {
StatusCode int
Body string
}
func (e *APIError) Error() string {
return fmt.Sprintf("ElevenLabs API error (status %d): %s", e.StatusCode, utils.Truncate(e.Body, 500))
}
return nil, &APIError{StatusCode: resp.StatusCode, Body: string(body)} Defensive patterns
Strategy: try-catch
Validate before calling
// Pre-flight the key without spending a transcription call:
if t.apiKey == "" { return errors.New("ElevenLabs API key missing") }
// Validate audio constraints before upload (Scribe limits):
fi, err := os.Stat(path)
if err != nil { return err }
if fi.Size() > 500*1024*1024 { return errors.New("audio too large for Scribe") }
switch strings.ToLower(filepath.Ext(path)) {
case ".wav", ".mp3", ".m4a", ".mp4", ".webm", ".flac", ".ogg", ".opus", ".aac":
default:
return fmt.Errorf("possibly unsupported audio extension: %s", filepath.Ext(path))
} Type guard
func elevenLabsStatus(err error) (int, string, bool) {
m := regexp.MustCompile(`ElevenLabs API error \(status (\d+)\): (.*)`).FindStringSubmatch(err.Error())
if m == nil { return 0, "", false }
code, _ := strconv.Atoi(m[1])
return code, m[2], true
} Try / catch
if _, err := el.Transcribe(ctx, path); err != nil {
if code, body, ok := elevenLabsStatus(err); ok {
switch {
case code == 401: return errors.New("invalid ElevenLabs API key")
case code == 422: return fmt.Errorf("audio rejected: %s", body)
case code == 429: time.Sleep(30 * time.Second) /* honor Retry-After */ ; return retry()
case code >= 500: return retryWithBackoff()
}
}
return err
} Prevention
- Never send an empty API key; assert it at startup.
- Normalize audio to 16kHz WAV/Opus before upload to avoid 422 format rejections.
- Throttle concurrent transcriptions to the plan's concurrency to avoid 429.
- Log the embedded response body verbatim — it names the exact violated constraint.
When it happens
Trigger: t.apiKey empty or revoked (401); audio format outside Scribe's supported set (422 with file_mime_type error); recording exceeds size/duration limits (422); modelID not scribe_v1 (constructor forces scribe_v1, so mainly custom apiBase deployments); burst traffic hitting free-tier concurrency (429); ElevenLabs incident (5xx).
Common situations: API key from a deleted workspace; 8kHz telephony WAV or exotic codec rejected; long dictations over the size cap; parallel transcriptions exceeding the plan's concurrency; status page incident.
Related errors
- HTTP %d: %s
- Request failed with status ${res.status}
- API error %d: %s
- error fetching models: %w
- failed to get WeCom QR code: errcode=%d errmsg=%s
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/0162a9762934d85a.
Report an issue: GitHub.