Billionmail/BillionMail · error
lipsync status API error %d: %s
Error message
lipsync status API error %d: %s
What it means
CheckLipSyncStatus treats any non-200 response from the Sync Labs status endpoint as an error, embedding the status code and the raw response body. This surfaces API-side rejections: invalid or missing x-api-key (401/403), unknown job ID (404), rate limiting (429), or server errors (5xx). The body text is included so the upstream message (e.g. 'job not found') is visible.
Source
Thrown at core/internal/service/video_gen/lipsync.go:151
}
// CheckLipSyncStatus checks the status of a lip sync job.
func CheckLipSyncStatus(ctx context.Context, cfg LipSyncConfig, jobID string) (*LipSyncResponse, error) {
httpReq, err := BuildLipSyncStatusRequest(cfg, jobID)
if err != nil {
return nil, err
}
httpReq = httpReq.WithContext(ctx)
resp, err := cfg.doHTTP(httpReq)
if err != nil {
return nil, fmt.Errorf("lipsync status API call: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("lipsync status API error %d: %s", resp.StatusCode, string(body))
}
var result LipSyncResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode lipsync status: %w", err)
}
return &result, nil
}
// DownloadLipSyncVideo downloads the completed lip sync video to the output directory.
func DownloadLipSyncVideo(ctx context.Context, cfg LipSyncConfig, videoURL, filename string) (string, error) {
if err := os.MkdirAll(cfg.OutputDir, 0755); err != nil {
return "", fmt.Errorf("create output dir: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "GET", videoURL, nil)
if err != nil {
return "", fmt.Errorf("create download request: %w", err)View on GitHub (pinned to fc36c76c05)
Solutions
- Check the status code and body in the error: 401/403 → fix SYNCLABS_API_KEY; 404 → verify the jobID came from a successful SubmitLipSync; 429 → add polling backoff; 5xx → retry later.
- Confirm the API key env var is set and valid before polling.
- Only poll job IDs returned by SubmitLipSync in the same run; persist them with their creation time if polling across restarts.
- If BaseURL is overridden, verify the URL matches the Sync Labs path layout (/lipsync/{id}).
Example fix
// before: polls immediately and reuses an unpersisted jobID
status, err := video_gen.CheckLipSyncStatus(ctx, cfg, oldJobID)
// after: poll a fresh jobID with exponential backoff on 429/5xx
for {
status, err := video_gen.CheckLipSyncStatus(ctx, cfg, jobID)
if err != nil {
if isRetryableStatusErr(err) { time.Sleep(backoff); backoff *= 2; continue }
return err
}
break
} Defensive patterns
Strategy: fallback
Validate before calling
if cfg.APIKey == "" { return errors.New("SYNCLABS_API_KEY not set") }
if jobID == "" { return errors.New("empty lipsync jobID") } Type guard
type statusAPIError struct{ Code int; Body string }
func asStatusAPIError(err error) (code int, body string, ok bool) {
m := regexp.MustCompile(`lipsync status API error (\d+): `).FindStringSubmatch(err.Error())
if m == nil { return 0, "", false }
code, _ = strconv.Atoi(m[1])
return code, strings.TrimPrefix(err.Error(), m[0]), true
} Try / catch
status, err := video_gen.CheckLipSyncStatus(ctx, cfg, jobID)
if err != nil {
if code, body, ok := asStatusAPIError(err); ok {
switch {
case code == http.StatusUnauthorized || code == http.StatusForbidden:
return fmt.Errorf("check SYNCLABS_API_KEY: %s", body)
case code == http.StatusNotFound:
return fmt.Errorf("lipsync job %s not found; resubmit", jobID)
case code == http.StatusTooManyRequests || code >= 500:
// back off and retry
}
}
return err
} Prevention
- Verify the API key at service startup, not per-poll.
- Only poll job IDs returned by SubmitLipSync; persist them if polling spans restarts.
- Add exponential backoff between polls to avoid 429s.
- Never reuse jobIDs from previous API versions or purged jobs.
When it happens
Trigger: Calling CheckLipSyncStatus with a jobID that does not exist or has expired, an empty/wrong SYNCLABS_API_KEY, a BaseURL override that routes to the wrong path, or hitting Sync Labs rate limits while polling many jobs in a tight loop.
Common situations: Stale jobID reused after the job record was purged server-side; API key rotated or set to a key from another Sync Labs account; polling loop without backoff triggering 429; typo'd BaseURL producing 404 HTML that gets embedded in the message.
Related errors
- lipsync API error %d: %s
- download error %d
- Failed to get account roles
- SMTP password is required
- unexpected signing method: %v
AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05).
Data as JSON: /api/errors/548bc658aac50d19.
Report an issue: GitHub.