Billionmail/BillionMail · error

lipsync status API error

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.

Solutions

  1. 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.
  2. Confirm the API key env var is set and valid before polling.
  3. Only poll job IDs returned by SubmitLipSync in the same run; persist them with their creation time if polling across restarts.
  4. 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

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


AI-assisted analysis of Billionmail/BillionMail@fc36c76c05 (2026-09-05). Data as JSON: /api/errors/548bc658aac50d19. Report an issue: GitHub.

Appendix: 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)