> For the documentation index, fetch https://sync.so/docs/llms.txt. Append .md to a page URL for Markdown. Documentation-search MCP: https://sync.so/docs/_mcp/server. # Python SDK Guide > Complete guide to the sync. labs Python SDK for AI lip sync. Installation, authentication, code examples, and best practices for lip sync generation with Python. The Sync Labs Python SDK (`syncsdk`) wraps the Sync Labs REST API with typed methods for Python 3.8+. It provides methods for creating lip sync generations, polling for results, estimating costs, and managing assets. Source code: [sync-python-sdk on GitHub](https://github.com/synchronicity-labs/sync-python-sdk) ## Installation Requires **Python 3.8+**. Install from PyPI: ```bash pip install syncsdk ``` > **Note** > > We recommend using a virtual environment to avoid dependency conflicts: > > ```bash > python -m venv .venv > source .venv/bin/activate # macOS/Linux > pip install syncsdk > ``` ## Authentication The SDK reads your API key from the `SYNC_API_KEY` environment variable automatically: ```bash export SYNC_API_KEY="your-api-key" ``` ```python from sync import Sync sync = Sync() # picks up SYNC_API_KEY from environment ``` You can also pass the key directly: ```python sync = Sync(api_key="your-api-key") ``` Create an API key from the [API Keys](https://sync.so/settings/api-keys) page in your dashboard. See the [Authentication](/api-reference/guides/authentication) guide for security best practices. ## Quick Start Create a lip sync generation, poll until it finishes, and print the output URL. **`quickstart.py`** ```python quickstart.py import time from sync import Sync from sync.common import Audio, Video, GenerationOptions from sync.core.api_error import ApiError sync = Sync() try: response = sync.generations.create( input=[ Video(url="https://assets.sync.so/docs/example-video.mp4"), Audio(url="https://assets.sync.so/docs/example-audio.wav"), ], model="lipsync-2", options=GenerationOptions(sync_mode="cut_off"), ) except ApiError as e: print(f"Request failed: {e.status_code} - {e.body}") exit() job_id = response.id print(f"Job submitted: {job_id}") # Poll until complete generation = sync.generations.get(job_id) while generation.status not in ["COMPLETED", "FAILED", "REJECTED"]: time.sleep(10) generation = sync.generations.get(job_id) if generation.status == "COMPLETED": print(f"Output: {generation.output_url}") else: print(f"Generation {job_id} failed: {generation.error}") ``` Run it: ```bash python quickstart.py ``` ## Core Operations ### Creating Generations #### Video + Audio The most common use case. Provide a video and an audio file, and Sync Labs generates matching lip movements. ```python from sync import Sync from sync.common import Audio, Video, GenerationOptions sync = Sync() response = sync.generations.create( input=[ Video(url="https://your-cdn.com/video.mp4"), Audio(url="https://your-cdn.com/audio.wav"), ], model="lipsync-2", options=GenerationOptions(sync_mode="cut_off"), output_file_name="my_output", ) ``` #### Video + Text-to-Speech Use the built-in ElevenLabs integration to go straight from text to lip-synced video. No separate TTS step needed. ```python from sync import Sync from sync.common import Video, TTS, GenerationOptions sync = Sync() response = sync.generations.create( input=[ Video(url="https://your-cdn.com/video.mp4"), TTS( provider={ "name": "elevenlabs", "voiceId": "EXAVITQu4vr4xnSDxMaL", "script": "Hello, this is a demo of the Sync Labs lip sync API.", "stability": 0.5, "similarityBoost": 0.75, } ), ], model="lipsync-2", options=GenerationOptions(sync_mode="cut_off"), ) ``` > **Note** > > The `script` field has a maximum of 5,000 characters per generation. For longer scripts, split them into segments. See the [Text-to-Speech Lip Sync Guide](/tutorials/text-to-speech-lipsync) for details. ### Polling vs Webhooks **Polling** is the simplest approach. Call `generations.get()` in a loop until the status is terminal: ```python import time generation = sync.generations.get(job_id) while generation.status not in ["COMPLETED", "FAILED", "REJECTED"]: time.sleep(10) generation = sync.generations.get(job_id) ``` **Webhooks** are better for production. Pass a `webhook_url` when creating the generation, and Sync Labs sends a POST request when the job finishes: ```python response = sync.generations.create( input=[ Video(url="https://your-cdn.com/video.mp4"), Audio(url="https://your-cdn.com/audio.wav"), ], model="lipsync-2", webhook_url="https://your-server.com/webhook", ) ``` See the [Webhooks](/api-reference/guides/webhooks) guide for payload format and signature verification. ### Listing Generations Retrieve your recent generations. Optionally filter by status: ```python # List all generations all_generations = sync.generations.list() # Filter by status completed = sync.generations.list(status="COMPLETED") pending = sync.generations.list(status="PENDING") for gen in completed: print(f"{gen.id} - {gen.status} - {gen.output_url}") ``` ### Getting Generation Details Fetch the full details of a single generation by ID: ```python generation = sync.generations.get("your-generation-id") print(f"Status: {generation.status}") print(f"Model: {generation.model}") print(f"Created: {generation.created_at}") print(f"Duration: {generation.output_duration}s") if generation.output_url: print(f"Output: {generation.output_url}") ``` ### Cost Estimation Estimate the cost before submitting a generation: ```python estimates = sync.generations.estimate_cost( input=[ Video(url="https://your-cdn.com/video.mp4"), Audio(url="https://your-cdn.com/audio.wav"), ], model="lipsync-2", ) for estimate in estimates: print(f"Estimated frames: {estimate.estimated_frame_count}") print(f"Estimated cost: ${estimate.estimated_generation_cost:.2f}") ``` ## Working with Files ### URL Inputs The simplest approach -- pass publicly accessible URLs for your video and audio: ```python from sync.common import Video, Audio Video(url="https://your-cdn.com/video.mp4") Audio(url="https://your-cdn.com/audio.wav") ``` ### Asset IDs If you have uploaded files to the Sync Labs media library (via the dashboard or Assets API), reference them by asset ID: ```python from sync.common import Video, Audio Video(asset_id="550e8400-e29b-41d4-a716-446655440000") Audio(asset_id="660e8400-e29b-41d4-a716-446655440001") ``` ### File Uploads Upload local files directly using the multipart endpoint: ```python with open("video.mp4", "rb") as video_file, open("audio.wav", "rb") as audio_file: response = sync.generations.create_with_files( video=video_file, audio=audio_file, model="lipsync-2", ) ``` > **Warning** > > File uploads are limited to 20MB per file. For larger files, host them at a public URL and use the URL-based input instead. ### Browsing Assets List and retrieve assets from your media library: ```python # List assets with pagination assets = sync.assets.list(limit=10, sort_by="dateDesc") for asset in assets.items: print(f"{asset.name} ({asset.type}) - {asset.duration_seconds}s") # Get a specific asset asset = sync.assets.get("550e8400-e29b-41d4-a716-446655440000") print(f"URL: {asset.url}") ``` ## Error Handling The SDK raises `ApiError` for HTTP errors. Wrap your calls in try/except blocks: ```python from sync import Sync from sync.core.api_error import ApiError sync = Sync() try: response = sync.generations.create( input=[ Video(url="https://your-cdn.com/video.mp4"), Audio(url="https://your-cdn.com/audio.wav"), ], model="lipsync-2", ) except ApiError as e: print(f"Status: {e.status_code}") print(f"Error: {e.body}") ``` ### Common Errors | Status Code | Cause | Fix | | :---------- | :---------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------- | | 400 | Invalid input (bad URL, unsupported format, missing fields) | Check your input parameters and media formats | | 401 | Missing or invalid API key | Verify `SYNC_API_KEY` is set correctly | | 402 | Feature requires a higher plan | Upgrade your plan at [sync.so](https://sync.so) | | 429 | Rate or concurrency limit exceeded | Wait and retry, or reduce concurrency | | 500 | Server error | For creation, follow [idempotent retry guidance](/api-reference/guides/idempotency); confirm an unkeyed outcome before resubmitting | ### Retry Logic For generation creation, use the [Idempotent Requests](/api-reference/guides/idempotency) contract to protect retries after timeouts and uncertain failures. Save the key before the first attempt and preserve it and the inputs across retries. The guide provides raw HTTP examples without depending on SDK-version-specific header options. The unkeyed example below retries only explicit HTTP 429 rejections. It does not retry 5xx or transport failures, which may have an unknown submission outcome: ```python import time from sync.core.api_error import ApiError def create_with_retry(sync, max_retries=3, **kwargs): for attempt in range(max_retries): try: return sync.generations.create(**kwargs) except ApiError as e: if e.status_code == 429 and attempt < max_retries - 1: wait = 2 ** attempt * 5 # 5s, 10s, 20s print(f"Retrying in {wait}s (attempt {attempt + 1}/{max_retries})") time.sleep(wait) else: raise ``` ## Async Usage The SDK includes an async client for use with `asyncio`. Use `AsyncSync` instead of `Sync Labs`: ```python import asyncio from sync import AsyncSync from sync.common import Audio, Video async def main(): sync = AsyncSync() response = await sync.generations.create( input=[ Video(url="https://your-cdn.com/video.mp4"), Audio(url="https://your-cdn.com/audio.wav"), ], model="lipsync-2", ) job_id = response.id print(f"Job submitted: {job_id}") # Poll until complete generation = await sync.generations.get(job_id) while generation.status not in ["COMPLETED", "FAILED", "REJECTED"]: await asyncio.sleep(10) generation = await sync.generations.get(job_id) if generation.status == "COMPLETED": print(f"Output: {generation.output_url}") asyncio.run(main()) ``` The async client supports the same methods as the sync client -- `create`, `get`, `list`, `estimate_cost`, and `create_with_files`. > **Note** > > The async client works with any `asyncio`-compatible framework like FastAPI, Sanic, or aiohttp. ## Type Hints The SDK is fully typed. All request parameters, response objects, and enums have type annotations. Your editor will provide autocomplete and inline documentation out of the box. ```python from sync.common import ( Audio, Video, TTS, GenerationOptions, Generation, GenerationStatus, Model, ) # Response objects are typed -- your editor knows all available fields generation: Generation = sync.generations.get("job-id") status: GenerationStatus = generation.status # "PENDING" | "PROCESSING" | "COMPLETED" | "FAILED" | "REJECTED" model: Model = generation.model # "lipsync-2" | "lipsync-2-pro" | ... output_url: str | None = generation.output_url ``` ## Available Models | Model | Best For | Speed | | :------------------- | :-------------------------------------------- | :------ | | `lipsync-2` | General purpose lip sync | Fast | | `lipsync-2-pro` | Premium content, fine detail (beards, teeth) | Slower | | `lipsync-1.9.0-beta` | Maximum speed | Fastest | | `react-1` | Expressive results with emotion (short clips) | Fast | See the [Models](/models/lipsync) page for detailed comparisons. ## Frequently Asked Questions #### Does the Python SDK support async? Yes. The SDK includes an AsyncSync client for use with asyncio. Import AsyncSync instead of Sync Labs and use await with all SDK methods. The async client supports the same operations as the sync client and works with frameworks like FastAPI, Sanic, and aiohttp. #### How do I handle timeouts? Wrap SDK calls in try/except blocks. A generation create timeout can mean the server accepted the request. Retry with the same key and payload if the first attempt used an [Idempotency-Key](/api-reference/guides/idempotency); otherwise confirm the outcome before submitting again. Poll an existing generation ID for status instead of creating another generation. #### Can I cancel a generation? The Sync Labs API does not currently provide a cancel endpoint for in-progress generations. Once a generation is submitted, it will run to completion or fail. You only pay for successfully completed generations. Monitor status via polling or webhooks to track progress. > Complete guide to the sync. labs Python SDK for AI lip sync. Installation, authentication, code examples, and best practices for lip sync generation with Python.