> 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.