{"record":{"id":"3bbaacc2189c1e60","repo":"Billionmail/BillionMail","slug":"cartesia-clone-api-error-d-s","errorCode":null,"errorMessage":"cartesia clone API error %d: %s","messagePattern":"cartesia clone API error (.+?): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/internal/service/video_gen/voice.go","lineNumber":156,"sourceCode":"\t\tAudioURL:    audioURL,\n\t\tLanguage:    \"en\",\n\t}\n\n\thttpReq, err := BuildCloneRequest(cfg, req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\thttpReq = httpReq.WithContext(ctx)\n\n\tresp, err := cfg.doHTTP(httpReq)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"cartesia clone API call: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn nil, fmt.Errorf(\"cartesia clone API error %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar result VoiceCloneResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {\n\t\treturn nil, fmt.Errorf(\"decode clone response: %w\", err)\n\t}\n\treturn &result, nil\n}\n\n// TextToSpeech generates audio from text using a Cartesia voice.\n// Returns the path to the output WAV file.\nfunc TextToSpeech(ctx context.Context, cfg VoiceConfig, voiceID, transcript, filename string) (string, error) {\n\tif err := os.MkdirAll(cfg.OutputDir, 0755); err != nil {\n\t\treturn \"\", fmt.Errorf(\"create output dir: %w\", err)\n\t}\n\n\treq := TTSRequest{\n\t\tVoiceID:      voiceID,","sourceCodeStart":138,"sourceCodeEnd":174,"githubUrl":"https://github.com/Billionmail/BillionMail/blob/fc36c76c050c3775c5e899faf7403cf0262d2744/core/internal/service/video_gen/voice.go#L138-L174","documentation":"CloneVoice in the video_gen package calls the Cartesia /voices/clone endpoint and requires an exact HTTP 200 response. When Cartesia returns any other status code (401, 400, 429, 5xx, etc.), the non-200 body is read and wrapped in this error, so the message contains the raw API error payload from Cartesia. It is a remote-API rejection of the clone request, not a local failure.","triggerScenarios":"Calling CloneVoice when the X-API-Key is invalid/expired (401), the clip audio URL is unreachable or an invalid format (400), quota/rate limits are hit (429), or Cartesia has a server-side outage (5xx).","commonSituations":"Missing or misconfigured CARTESIA_API_KEY env var, audio clip URL behind auth or expired presigned URL, unsupported audio format/language, exceeding account voice-clone limits, Cartesia API version incompatibility.","solutions":["Log the embedded Cartesia body — it names the exact reason (invalid key, bad clip, quota).","Verify CARTESIA_API_KEY is set and valid for the account.","Check the audioURL is publicly fetchable, uses a supported audio format, and is not expired.","Check Cartesia status/quota; apply backoff and retry for 429/5xx.","Confirm the Cartesia-Version header (2024-06-10) is still accepted by your account."],"exampleFix":"// before\nresp, err := cfg.doHTTP(httpReq)\nif err != nil { ... }\nif resp.StatusCode != http.StatusOK {\n    return nil, fmt.Errorf(\"cartesia clone API error %d: %s\", resp.StatusCode, string(body))\n}\n// after\nif cfg.APIKey == \"\" {\n    return nil, errors.New(\"cartesia API key missing\")\n}\nif u, err := url.Parse(audioURL); err != nil || u.Scheme != \"https\" {\n    return nil, fmt.Errorf(\"invalid audio URL %q\", audioURL)\n}\nresp, err := cfg.doHTTP(httpReq)\nif err != nil { ... }\nswitch {\ncase resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= 500:\n    return nil, retryableError{fmt.Errorf(\"cartesia clone API error %d\", resp.StatusCode)}\ncase resp.StatusCode != http.StatusOK:\n    return nil, fmt.Errorf(\"cartesia clone API error %d: %s\", resp.StatusCode, string(body))\n}","handlingStrategy":"validation","validationCode":"if cfg.APIKey == \"\" {\n    return errors.New(\"CARTESIA_API_KEY is not set\")\n}\nif audioURL == \"\" {\n    return errors.New(\"audioURL is required for voice cloning\")\n}\nu, err := url.Parse(audioURL)\nif err != nil || (u.Scheme != \"https\" && u.Scheme != \"http\") {\n    return fmt.Errorf(\"invalid audio URL: %q\", audioURL)\n}\nresp, err := http.Head(audioURL)\nif err == nil && resp.StatusCode != http.StatusOK {\n    return fmt.Errorf(\"audio URL not fetchable: %d\", resp.StatusCode)\n}","typeGuard":null,"tryCatchPattern":"resp, err := video_gen.CloneVoice(ctx, cfg, name, audioURL)\nif err != nil {\n    var apiErr *video_gen.APIError\n    if strings.Contains(err.Error(), \"cartesia clone API error 4\") {\n        // non-retryable: fix key/URL/input\n        log.Fatalf(\"clone rejected: %v\", err)\n    }\n    // 429/5xx: retry with backoff\n}","preventionTips":["Set and verify CARTESIA_API_KEY before deploying","Pre-check audio clip URLs are public, https, and unexpired","Monitor 429 responses and back off instead of hammering the API","Keep the Cartesia-Version header aligned with the account's supported version"],"tags":["http","api","cartesia","voice-clone"],"backgroundTag":"upstream-api-non-2xx","analyzedSha":"fc36c76c050c3775c5e899faf7403cf0262d2744","analyzedAt":"2026-09-05T21:28:54.019Z","contentChangedAt":"2026-09-05T21:28:54.019Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}