Python SDK Guide

The Sync Python SDK (syncsdk) wraps the Sync 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

Installation

Requires Python 3.8+. Install from PyPI:

$pip install syncsdk

We recommend using a virtual environment to avoid dependency conflicts:

$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:

$export SYNC_API_KEY="your-api-key"
1from sync import Sync
2
3sync = Sync() # picks up SYNC_API_KEY from environment

You can also pass the key directly:

1sync = Sync(api_key="your-api-key")

Create an API key from the API Keys page in your dashboard. See the Authentication guide for security best practices.

Quick Start

Create a lip sync generation, poll until it finishes, and print the output URL.

quickstart.py
1import time
2from sync import Sync
3from sync.common import Audio, Video, GenerationOptions
4from sync.core.api_error import ApiError
5
6sync = Sync()
7
8try:
9 response = sync.generations.create(
10 input=[
11 Video(url="https://assets.sync.so/docs/example-video.mp4"),
12 Audio(url="https://assets.sync.so/docs/example-audio.wav"),
13 ],
14 model="lipsync-2",
15 options=GenerationOptions(sync_mode="cut_off"),
16 )
17except ApiError as e:
18 print(f"Request failed: {e.status_code} - {e.body}")
19 exit()
20
21job_id = response.id
22print(f"Job submitted: {job_id}")
23
24# Poll until complete
25generation = sync.generations.get(job_id)
26while generation.status not in ["COMPLETED", "FAILED", "REJECTED"]:
27 time.sleep(10)
28 generation = sync.generations.get(job_id)
29
30if generation.status == "COMPLETED":
31 print(f"Output: {generation.output_url}")
32else:
33 print(f"Generation {job_id} failed: {generation.error}")

Run it:

$python quickstart.py

Core Operations

Creating Generations

Video + Audio

The most common use case. Provide a video and an audio file, and Sync generates matching lip movements.

1from sync import Sync
2from sync.common import Audio, Video, GenerationOptions
3
4sync = Sync()
5
6response = sync.generations.create(
7 input=[
8 Video(url="https://your-cdn.com/video.mp4"),
9 Audio(url="https://your-cdn.com/audio.wav"),
10 ],
11 model="lipsync-2",
12 options=GenerationOptions(sync_mode="cut_off"),
13 output_file_name="my_output",
14)

Video + Text-to-Speech

Use the built-in ElevenLabs integration to go straight from text to lip-synced video. No separate TTS step needed.

1from sync import Sync
2from sync.common import Video, TTS, GenerationOptions
3
4sync = Sync()
5
6response = sync.generations.create(
7 input=[
8 Video(url="https://your-cdn.com/video.mp4"),
9 TTS(
10 provider={
11 "name": "elevenlabs",
12 "voiceId": "EXAVITQu4vr4xnSDxMaL",
13 "script": "Hello, this is a demo of the Sync lip sync API.",
14 "stability": 0.5,
15 "similarityBoost": 0.75,
16 }
17 ),
18 ],
19 model="lipsync-2",
20 options=GenerationOptions(sync_mode="cut_off"),
21)

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

Polling vs Webhooks

Polling is the simplest approach. Call generations.get() in a loop until the status is terminal:

1import time
2
3generation = sync.generations.get(job_id)
4while generation.status not in ["COMPLETED", "FAILED", "REJECTED"]:
5 time.sleep(10)
6 generation = sync.generations.get(job_id)

Webhooks are better for production. Pass a webhook_url when creating the generation, and Sync sends a POST request when the job finishes:

1response = sync.generations.create(
2 input=[
3 Video(url="https://your-cdn.com/video.mp4"),
4 Audio(url="https://your-cdn.com/audio.wav"),
5 ],
6 model="lipsync-2",
7 webhook_url="https://your-server.com/webhook",
8)

See the Webhooks guide for payload format and signature verification.

Listing Generations

Retrieve your recent generations. Optionally filter by status:

1# List all generations
2all_generations = sync.generations.list()
3
4# Filter by status
5completed = sync.generations.list(status="COMPLETED")
6pending = sync.generations.list(status="PENDING")
7
8for gen in completed:
9 print(f"{gen.id} - {gen.status} - {gen.output_url}")

Getting Generation Details

Fetch the full details of a single generation by ID:

1generation = sync.generations.get("your-generation-id")
2
3print(f"Status: {generation.status}")
4print(f"Model: {generation.model}")
5print(f"Created: {generation.created_at}")
6print(f"Duration: {generation.output_duration}s")
7
8if generation.output_url:
9 print(f"Output: {generation.output_url}")

Cost Estimation

Estimate the cost before submitting a generation:

1estimates = sync.generations.estimate_cost(
2 input=[
3 Video(url="https://your-cdn.com/video.mp4"),
4 Audio(url="https://your-cdn.com/audio.wav"),
5 ],
6 model="lipsync-2",
7)
8
9for estimate in estimates:
10 print(f"Estimated frames: {estimate.estimated_frame_count}")
11 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:

1from sync.common import Video, Audio
2
3Video(url="https://your-cdn.com/video.mp4")
4Audio(url="https://your-cdn.com/audio.wav")

Asset IDs

If you have uploaded files to the Sync media library (via the dashboard or Assets API), reference them by asset ID:

1from sync.common import Video, Audio
2
3Video(asset_id="550e8400-e29b-41d4-a716-446655440000")
4Audio(asset_id="660e8400-e29b-41d4-a716-446655440001")

File Uploads

Upload local files directly using the multipart endpoint:

1with open("video.mp4", "rb") as video_file, open("audio.wav", "rb") as audio_file:
2 response = sync.generations.create_with_files(
3 video=video_file,
4 audio=audio_file,
5 model="lipsync-2",
6 )

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:

1# List assets with pagination
2assets = sync.assets.list(limit=10, sort_by="dateDesc")
3for asset in assets.items:
4 print(f"{asset.name} ({asset.type}) - {asset.duration_seconds}s")
5
6# Get a specific asset
7asset = sync.assets.get("550e8400-e29b-41d4-a716-446655440000")
8print(f"URL: {asset.url}")

Error Handling

The SDK raises ApiError for HTTP errors. Wrap your calls in try/except blocks:

1from sync import Sync
2from sync.core.api_error import ApiError
3
4sync = Sync()
5
6try:
7 response = sync.generations.create(
8 input=[
9 Video(url="https://your-cdn.com/video.mp4"),
10 Audio(url="https://your-cdn.com/audio.wav"),
11 ],
12 model="lipsync-2",
13 )
14except ApiError as e:
15 print(f"Status: {e.status_code}")
16 print(f"Error: {e.body}")

Common Errors

Status CodeCauseFix
400Invalid input (bad URL, unsupported format, missing fields)Check your input parameters and media formats
401Missing or invalid API keyVerify SYNC_API_KEY is set correctly
402Feature requires a higher planUpgrade your plan at sync.so
429Rate or concurrency limit exceededWait and retry, or reduce concurrency
500Server errorRetry after a short delay

Retry Logic

For production systems, add retry logic with exponential backoff:

1import time
2from sync.core.api_error import ApiError
3
4def create_with_retry(sync, max_retries=3, **kwargs):
5 for attempt in range(max_retries):
6 try:
7 return sync.generations.create(**kwargs)
8 except ApiError as e:
9 if e.status_code in (429, 500, 503) and attempt < max_retries - 1:
10 wait = 2 ** attempt * 5 # 5s, 10s, 20s
11 print(f"Retrying in {wait}s (attempt {attempt + 1}/{max_retries})")
12 time.sleep(wait)
13 else:
14 raise

Async Usage

The SDK includes an async client for use with asyncio. Use AsyncSync instead of Sync:

1import asyncio
2from sync import AsyncSync
3from sync.common import Audio, Video
4
5async def main():
6 sync = AsyncSync()
7
8 response = await sync.generations.create(
9 input=[
10 Video(url="https://your-cdn.com/video.mp4"),
11 Audio(url="https://your-cdn.com/audio.wav"),
12 ],
13 model="lipsync-2",
14 )
15
16 job_id = response.id
17 print(f"Job submitted: {job_id}")
18
19 # Poll until complete
20 generation = await sync.generations.get(job_id)
21 while generation.status not in ["COMPLETED", "FAILED", "REJECTED"]:
22 await asyncio.sleep(10)
23 generation = await sync.generations.get(job_id)
24
25 if generation.status == "COMPLETED":
26 print(f"Output: {generation.output_url}")
27
28asyncio.run(main())

The async client supports the same methods as the sync client — create, get, list, estimate_cost, and create_with_files.

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.

1from sync.common import (
2 Audio,
3 Video,
4 TTS,
5 GenerationOptions,
6 Generation,
7 GenerationStatus,
8 Model,
9)
10
11# Response objects are typed -- your editor knows all available fields
12generation: Generation = sync.generations.get("job-id")
13status: GenerationStatus = generation.status # "PENDING" | "PROCESSING" | "COMPLETED" | "FAILED" | "REJECTED"
14model: Model = generation.model # "lipsync-2" | "lipsync-2-pro" | ...
15output_url: str | None = generation.output_url

Available Models

ModelBest ForSpeed
lipsync-2General purpose lip syncFast
lipsync-2-proPremium content, fine detail (beards, teeth)Slower
lipsync-1.9.0-betaMaximum speedFastest
react-1Expressive results with emotion (short clips)Fast

See the Models page for detailed comparisons.

Next Steps

Frequently Asked Questions

Yes. The SDK includes an AsyncSync client for use with asyncio. Import AsyncSync instead of Sync 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.

The SDK raises ApiError for HTTP errors including timeouts. Wrap calls in try/except blocks and implement retry logic with exponential backoff for transient errors. For polling, use a sleep interval of 10 seconds between status checks to avoid hitting rate limits.

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