← Files sync. labsARCHIVED FILE
skills/sync-api/references/python.md
4.93 KB · Nov 6, 2026 · 18:14 UTC
# Python integration
`pip install syncsdk`. It brings in `httpx`, which the HTTP-only calls below also use. Python 3.8+.
```python
import json
import mimetypes
import os
import pathlib
import time
import uuid
import httpx
from sync import Sync
from sync.common import Audio, Video
from sync.core.api_error import ApiError
API = "https://api.sync.so/v2"
HEADERS = {"x-api-key": os.environ["SYNC_API_KEY"]}
client = Sync() # reads SYNC_API_KEY
def upload(path: str, kind: str) -> str:
"""Upload a local file into sync. storage and return its asset ID. kind: VIDEO, AUDIO or IMAGE."""
file = pathlib.Path(path)
content_type = mimetypes.guess_type(file.name)[0] or "application/octet-stream"
presign = httpx.post(f"{API}/assets/upload", headers=HEADERS, timeout=30,
json={"fileName": file.name, "contentType": content_type, "size": file.stat().st_size})
presign.raise_for_status()
target = presign.json()
# The presigned upload URL must not receive the API key.
with file.open("rb") as body:
httpx.put(target["uploadUrl"], content=body, headers={"Content-Type": content_type}, timeout=600).raise_for_status()
asset = httpx.post(f"{API}/assets", headers=HEADERS, timeout=30, json={"url": target["url"], "type": kind})
asset.raise_for_status()
return asset.json()["id"]
def estimate(model: str, seconds: float) -> float:
resp = httpx.post(f"{API}/generations/estimate", headers=HEADERS, timeout=30,
json={"model": model, "duration": seconds})
resp.raise_for_status()
return resp.json()["estimatedGenerationCost"] # USD
def start_lipsync(job: dict) -> str:
"""job is your persisted record: video/audio asset IDs and an idempotency key, all saved before this call."""
while True:
try:
generation = client.generations.create(
model="sync-3",
input=[Video(asset_id=job["video_asset_id"]), Audio(asset_id=job["audio_asset_id"])],
options={"sync_mode": "bounce"},
webhook_url=job.get("webhook_url"),
request_options={"additional_headers": {"Idempotency-Key": job["idempotency_key"]}},
)
return generation.id
except ApiError as err:
body = err.body if isinstance(err.body, dict) else {}
if err.status_code == 429:
time.sleep(float(body.get("retryAfterSeconds", 20)))
continue # same key, so no duplicate generation
if err.status_code == 409 and body.get("generationId"):
return body["generationId"]
raise
def wait_for(generation_id: str) -> str:
"""Polling fallback. Prefer a webhook in production."""
while True:
resp = httpx.get(f"{API}/generate/{generation_id}", headers=HEADERS, timeout=30,
params={"wait": "true", "timeout": 10})
resp.raise_for_status()
generation = resp.json()
if generation["status"] == "COMPLETED":
return generation["outputUrl"]
if generation["status"] in ("FAILED", "REJECTED"):
raise RuntimeError(f"{generation['status']}: {generation.get('errorCode')} {generation.get('error')}")
time.sleep(2)
JOB_FILE = pathlib.Path("lipsync-job.json") # stands in for a row in your database
def load_or_create_job() -> dict:
"""Save the asset IDs and the idempotency key before creating, so a restart can't pay twice."""
if JOB_FILE.exists():
return json.loads(JOB_FILE.read_text())
job = {
"video_asset_id": upload("talk.mp4", "VIDEO"),
"audio_asset_id": upload("new-take.wav", "AUDIO"),
"idempotency_key": str(uuid.uuid4()),
}
JOB_FILE.write_text(json.dumps(job))
return job
if __name__ == "__main__":
job = load_or_create_job()
if not job.get("generation_id"):
print("Estimated cost: $%.2f" % estimate("sync-3", 12.0))
job["generation_id"] = start_lipsync(job)
JOB_FILE.write_text(json.dumps(job))
# After a restart this polls the saved generation instead of creating another one.
print("Output:", wait_for(job["generation_id"]))
```
## Variations
- **Script instead of audio:** `from sync.common import Tts, ElevenLabs`, then `Tts(provider=ElevenLabs(voice_id="<voiceId>", script="Hello!"))` in place of `Audio(...)`. List voices with `GET /v2/voices`.
- **Image input, `dubParams`, `projectId`:** if your installed `syncsdk` doesn't type them yet, send the same JSON body with `httpx.post(f"{API}/generate", headers={**HEADERS, "Idempotency-Key": key}, json=body)`.
- **Async code:** `from sync import AsyncSync` has the same methods, awaited.
- **Batch:** `client.batch.create(input=open("rows.jsonl", "rb"), dry_run=True)`, then without `dry_run`. Batch creation isn't idempotent, so leave retries off (the Python SDK's default is 0), record that you're submitting before the call, and after a timeout check `GET /v2/batch` before sending it again. See `batch-api.md`.
SHA-256: 4f1902a9daecbe0f2f8f4f0872a0a9d7cf991373c809fce589b6b77ad3e603c2