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

  1. Read the embedded body in the error string — it states the exact violation (e.g. 'file_mime_type is not supported').
  2. 401: verify the XI-Api-Key value and that the key is active in the ElevenLabs dashboard.
  3. 422: convert the audio to a supported format (16kHz+ WAV/MP3/Opus) and check duration/size caps before upload.
  4. 429: back off (Retry-After header) and serialize or throttle transcription jobs to the plan's concurrency.
  5. 5xx: retry with exponential backoff; check status.elevenlabs.io.
  6. 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

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


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/0162a9762934d85a. Report an issue: GitHub.