Error Handling
Understanding error responses, HTTP status codes, rate limits, and troubleshooting common issues.
The CHAMELAION API uses standard HTTP status codes and returns consistent JSON error responses. This guide covers all error scenarios, rate limiting, and how to handle them in your application.
Error response format
Section titled “Error response format”All errors follow the same structure:
{ "error": "human-readable error message"}The error field always contains a descriptive message explaining what went wrong.
HTTP status codes
Section titled “HTTP status codes”| Status | Meaning | Description |
|---|---|---|
| 200 | Success | Request completed successfully |
| 400 | Bad Request | Invalid request body, missing fields, or wrong format |
| 401 | Unauthorized | Authentication failed or missing |
| 404 | Not Found | The requested Lip Sync or Video Translate request doesn’t exist |
| 408 | Request Timeout | Video Translate upload stalled for more than 30 seconds |
| 412 | Precondition Failed | Token limit reached. Upgrade your plan |
| 413 | Payload Too Large | Video Translate request exceeds the size limit |
| 429 | Too Many Requests | Rate limit or Video Translate concurrency limit exceeded |
| 500 | Internal Server Error | Something went wrong on our side |
400 - Bad Request errors
Section titled “400 - Bad Request errors”These indicate problems with your request format or content.
Lip Sync from URLs (/v1/lipsync/generate)
Section titled “Lip Sync from URLs (/v1/lipsync/generate)”| Error Message | Cause |
|---|---|
invalid request body | JSON is malformed or doesn’t match the schema |
content-type must be application/json | Missing or wrong Content-Type header |
inputs is required | inputs is missing or empty |
expected exactly one video input | inputs doesn’t contain exactly one video item |
expected exactly one audio input | inputs doesn’t contain exactly one audio item |
input type must be "video" or "audio" | An input has an unrecognized type value |
unable to access media url | CHAMELAION could not download a media URL |
concurrency limit reached for your plan: ... | Too many unfinished lip sync requests for your plan |
Lip Sync file upload (/v1/lipsync/generate-with-media)
Section titled “Lip Sync file upload (/v1/lipsync/generate-with-media)”| Error Message | Cause |
|---|---|
content-type must be multipart/form-data | Wrong content type. Must be multipart/form-data |
video file is required | No video field in the form data |
audio file is required | No audio field in the form data |
uploaded file is not a valid video input | The file isn’t sent as video/* (or audio/* for the audio part) |
concurrency limit reached for your plan: ... | Too many unfinished lip sync requests for your plan |
Video Translate (/v1/video-translate/*)
Section titled “Video Translate (/v1/video-translate/*)”Video Translate checks settings, subtitle syntax, and voice IDs before creating the request. Common errors:
target_languages must contain at least one language:target_languagesis missing or empty.target_languages must not contain more than 10 languages: Too many target languages.unsupported target language "xx": The code isn’t a supported language.source_language must not be included in target_languages: The source language is also listed as a target.video URL is not accessible: CHAMELAION could not download the video URL.input must be a video file: The URL or upload isn’t a video.<field> is not a valid SRT file: ...: A subtitle file has a syntax error. The message names the field (for examplesource_srtortarget_srt.de) and the line.speaker_voices references unknown or unavailable voice IDs: ...: A voice ID isn’t listed byGET /v1/video-translate/voices.
The API reference lists every validation rule.
Example: handling 400 errors
Section titled “Example: handling 400 errors”import requests
response = requests.post( "https://api.chamelaion.com/api/v1/lipsync/generate", headers={ "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", }, json={"inputs": []}, # Invalid: empty inputs)
if response.status_code == 400: error = response.json() print(f"Bad request: {error['error']}") # Bad request: inputs is required401 - Authentication errors
Section titled “401 - Authentication errors”See the Authentication guide for details. Common causes:
| Error Message | Fix |
|---|---|
missing authorization header | Add Authorization: Bearer <token> or x-api-key |
invalid authorization format | Use Bearer <token> format (note the space after Bearer) |
missing token | Token value is empty |
invalid token | Token is expired, revoked, or incorrect |
404 - Not Found
Section titled “404 - Not Found”Returned when you request a job that doesn’t exist. The message is lipsync request not found or video translate request not found:
{ "error": "lipsync request not found"}Double-check the request_id or reference_id you’re using. Common causes:
- Typo in the ID
- The request belongs to a different account
- Using a
reference_idthat was never set
408 and 413 - Upload limits
Section titled “408 and 413 - Upload limits”Video Translate returns 413 when a JSON body exceeds 8 MiB or an upload body exceeds 2 GiB plus 9 MiB for form fields and SRT files. A video over 2 GiB returns 400 (video file exceeds the 2 GiB limit). An upload that sends no data for 30 seconds returns 408. See Limits.
412 - Token Limit Reached
Section titled “412 - Token Limit Reached”Your account’s generation quota has been exhausted:
{ "error": "token limit reached"}Upgrade your plan from the Dashboard to increase your quota.
429 - Rate Limit
Section titled “429 - Rate Limit”You’ve exceeded the request rate limit:
{ "error": "rate limit exceeded"}Video Translate also returns 429 when your account has too many active translations (too many active video translations for this account) or when capacity is temporarily full (video translation capacity is temporarily exhausted). Retry with backoff.
Handling rate limits with retry logic
Section titled “Handling rate limits with retry logic”import requestsimport time
def api_request_with_retry(method, url, max_retries=3, **kwargs): """Make an API request with automatic retry on rate limits.""" for attempt in range(max_retries + 1): response = requests.request(method, url, **kwargs)
if response.status_code == 429: wait_time = 2 ** attempt # 1s, 2s, 4s print(f"Rate limited. Retrying in {wait_time}s...") time.sleep(wait_time) continue
return response
raise Exception("Max retries exceeded due to rate limiting")async function apiRequestWithRetry( url: string, options: RequestInit, maxRetries = 3): Promise<Response> { for (let attempt = 0; attempt <= maxRetries; attempt++) { const response = await fetch(url, options);
if (response.status === 429) { const waitTime = 2 ** attempt * 1000; console.log(`Rate limited. Retrying in ${waitTime}ms...`); await new Promise((r) => setTimeout(r, waitTime)); continue; }
return response; }
throw new Error("Max retries exceeded due to rate limiting");}500 - Internal Server Error
Section titled “500 - Internal Server Error”These indicate a problem on our side:
{ "error": "internal error"}If you encounter 500 errors:
- Retry the request after a short delay
- If the error persists, check the status page
- Contact support at support@chamelaion.com with the request details
Comprehensive error handling example
Section titled “Comprehensive error handling example”Here’s a robust Python function that handles all error types:
import requestsimport timeimport os
class CHAMELAIONError(Exception): def __init__(self, status_code: int, message: str): self.status_code = status_code self.message = message super().__init__(f"[{status_code}] {message}")
class RateLimitError(CHAMELAIONError): pass
class AuthenticationError(CHAMELAIONError): pass
class QuotaExceededError(CHAMELAIONError): pass
def chamelaion_request(method: str, path: str, max_retries: int = 3, **kwargs): """Make a request to the CHAMELAION API with full error handling.""" base_url = "https://api.chamelaion.com/api" headers = {"Authorization": f"Bearer {os.environ['CHAMELAION_API_KEY']}"} kwargs.setdefault("headers", {}).update(headers)
for attempt in range(max_retries + 1): response = requests.request(method, f"{base_url}{path}", **kwargs)
if response.status_code == 200: return response.json()
error_msg = response.json().get("error", "unknown error")
if response.status_code == 429: if attempt < max_retries: wait = 2 ** attempt time.sleep(wait) continue raise RateLimitError(429, error_msg)
if response.status_code == 401: raise AuthenticationError(401, error_msg)
if response.status_code == 412: raise QuotaExceededError(412, error_msg)
if response.status_code == 500 and attempt < max_retries: time.sleep(2 ** attempt) continue
raise CHAMELAIONError(response.status_code, error_msg)Troubleshooting
Section titled “Troubleshooting”My generation is stuck in “queued” status
Jobs may stay in queued status during high-traffic periods. If a job remains queued for more than 10 minutes, contact support. Make sure your polling timeout is at least 10 minutes for production workloads.
The output video has no lip movement
This can happen if CHAMELAION cannot detect a face in the video. Ensure your video has a clearly visible, front-facing speaker. For multi-person videos, make sure disable_active_speaker_detection is false so the correct speaker is identified.
My media URLs are rejected as not accessible
Lip Sync returns unable to access media url and Video Translate returns video URL is not accessible when CHAMELAION’s servers can’t download your media. Ensure:
- The URLs are publicly accessible (no authentication required)
- The URLs don’t have IP restrictions or geo-blocking
- The URLs return the actual media content (not an HTML page)
- Pre-signed URLs haven’t expired
My video translation failed after it was accepted
Some checks run only after the video is processed. Read error_message on the request, then:
- If the source language couldn’t be detected, set
source_languageexplicitly - If the detected source language is also a target, remove it from
target_languages - If SRT validation failed, make sure source and target files have the same number of cues and speakers, and that timings fit within the video
- If
speaker_voicesfailed, make sure every key matches aSpeaker N:number in yoursource_srt
I keep getting 401 errors
- Verify your API key in the Dashboard
- Check that you’re using the correct header format (
Bearer <token>orx-api-key: <token>) - The
Bearerprefix is case-sensitive - Confirm the key hasn’t been revoked