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

  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.