Saltar a la navegación

TypeScript SDK Guide

The Sync Labs TypeScript SDK (@sync.so/sdk) provides typed methods for Node.js 18+ applications. It wraps the full Sync Labs API surface for creating lip sync generations, polling status, and retrieving results from any TypeScript/JavaScript runtime.

Installation

Requires Node.js 18+. The SDK ships with both ESM and CommonJS builds, so it works with any module system.

# npm
npm i @sync.so/sdk
# yarn
yarn add @sync.so/sdk
# pnpm
pnpm add @sync.so/sdk

This guide covers @sync.so/sdk 0.4.0 and later. Check your version with npm ls @sync.so/sdk and upgrade with npm i @sync.so/sdk@latest. Version 0.4.0 adds image inputs, dubParams, dialogueEdit and projectId on generation create, asset uploads, projects, voices, text-to-speech, transcriptions, dialogue edits, account.get(), feedback.submit(), outputAudio and lipsync: false segments, and a working estimateCost. In 0.4.0, estimateCost takes a model and a duration (it previously took a full generation request), and generations.delete returns the stored-generation shape.

Authentication

The SDK reads your API key from the SYNC_API_KEY environment variable automatically.

export SYNC_API_KEY="your-api-key"

Create a key from the API Keys page in your dashboard.

You can also pass the key explicitly:

import { SyncClient } from "@sync.so/sdk";
// Reads SYNC_API_KEY from environment
const sync = new SyncClient();
// Or pass it directly
const sync = new SyncClient({ apiKey: "your-api-key" });

Never expose your API key in client-side code. The SDK is designed for server-side use only. See Framework Integration for details.

Quick Start

Create a lip sync generation, poll for completion, and get the output URL:

quickstart.ts
import { SyncClient, SyncError } from "@sync.so/sdk";
const sync = new SyncClient();
async function main() {
// 1. Submit a generation
const response = await sync.generations.create({
input: [
{ type: "video", url: "https://assets.sync.so/docs/example-video.mp4" },
{ type: "audio", url: "https://assets.sync.so/docs/example-audio.wav" },
],
model: "lipsync-2",
options: { sync_mode: "cut_off" },
});
const jobId = response.id;
console.log(`Job submitted: ${jobId}`);
// 2. Poll until complete
let generation = await sync.generations.get(jobId);
while (!["COMPLETED", "FAILED", "REJECTED"].includes(generation.status)) {
await new Promise((r) => setTimeout(r, 10000));
generation = await sync.generations.get(jobId);
}
// 3. Get output
if (generation.status === "COMPLETED") {
console.log(`Output: ${generation.outputUrl}`);
} else {
console.log(`Generation ${jobId} failed`);
}
}
main();

Run it with:

npx tsx quickstart.ts

Core Operations

Creating Generations

Video + Audio

The most common pattern. Provide a source video and an audio track — Sync Labs generates lip movements matching the audio.

const response = await sync.generations.create({
input: [
{ type: "video", url: "https://your-cdn.com/video.mp4" },
{ type: "audio", url: "https://your-cdn.com/audio.wav" },
],
model: "lipsync-2",
options: { sync_mode: "cut_off" },
outputFileName: "my-output",
});

Video + Text-to-Speech

Use the built-in ElevenLabs integration to go from text to lip-synced video in a single call:

const response = await sync.generations.create({
input: [
{
type: "video",
url: "https://your-cdn.com/video.mp4",
},
{
type: "text",
provider: {
name: "elevenlabs",
voiceId: "EXAVITQu4vr4xnSDxMaL",
script: "Your script goes here. Max 5,000 characters per generation.",
stability: 0.5,
similarityBoost: 0.75,
},
},
],
model: "lipsync-2",
options: { sync_mode: "cut_off" },
});

Enable the ElevenLabs integration from your Integrations settings before using the TTS input type.

Polling vs Webhooks

The quick start above uses polling. For production systems, webhooks are more efficient — you receive a POST notification when the job finishes instead of making repeated API calls.

// With webhooks, pass a URL when creating the generation
const response = await sync.generations.create({
input: [
{ type: "video", url: "https://your-cdn.com/video.mp4" },
{ type: "audio", url: "https://your-cdn.com/audio.wav" },
],
model: "lipsync-2",
webhookUrl: "https://your-app.com/api/sync-webhook",
});
// No polling needed -- your webhook endpoint receives the result

Listing Generations

Retrieve your recent generations:

const generations = await sync.generations.list();
for (const gen of generations) {
console.log(`${gen.id} - ${gen.status}`);
}

Getting Generation Details

Fetch a specific generation by ID:

const generation = await sync.generations.get("generation-id");
console.log(`Status: ${generation.status}`);
console.log(`Output: ${generation.outputUrl}`);
console.log(`Model: ${generation.model}`);

Cost Estimation

Estimate a generation from its model and billable duration in seconds. The response is a single object; estimatedCredits is null for dollar-billed organizations, and durationCheck says whether your plan allows that duration.

const estimate = await sync.generations.estimateCost({ model: "lipsync-2", duration: 10, fps: 30 });
console.log(`Estimated cost: $${estimate.estimatedGenerationCost}`);
console.log(`Estimated frames: ${estimate.estimatedFrameCount}`);
console.log(`Estimated credits: ${estimate.estimatedCredits}`);

Pass workflow: "translate-and-dub" to quote dubbing plus lip sync; the response then carries a breakdown.

Working with Files

URL Inputs

The simplest approach. Provide publicly accessible URLs for your video and audio files:

const response = await sync.generations.create({
input: [
{ type: "video", url: "https://your-cdn.com/video.mp4" },
{ type: "audio", url: "https://your-cdn.com/audio.wav" },
],
model: "lipsync-2",
});

Image Inputs

sync-3 can animate a still image instead of a video:

const response = await sync.generations.create({
input: [
{ type: "image", url: "https://your-cdn.com/portrait.png" },
{ type: "audio", url: "https://your-cdn.com/audio.wav" },
],
model: "sync-3",
});

Uploading Files via the Assets API

If your files are not hosted at public URLs, upload them as assets first and pass the asset ID. See Asset Uploads for the full flow.

import { readFile } from "node:fs/promises";
const bytes = await readFile("video.mp4");
// 1. Request a presigned URL
const presign = await sync.assets.createUpload({
fileName: "video.mp4",
contentType: "video/mp4",
size: bytes.length,
});
// 2. PUT the bytes, with the same Content-Type and no API key
const put = await fetch(presign.uploadUrl, {
method: "PUT",
headers: { "Content-Type": "video/mp4" },
body: bytes,
});
if (!put.ok) throw new Error(`Upload failed: ${put.status}`);
// 3. Register the asset and generate from its ID
const asset = await sync.assets.create({ url: presign.url, type: "VIDEO", name: "video.mp4" });
const response = await sync.generations.create({
input: [
{ type: "video", assetId: asset.id },
{ type: "audio", url: "https://your-cdn.com/audio.wav" },
],
model: "lipsync-2",
});

For direct file uploads, use the create with files endpoint.

Error Handling

The SDK throws SyncError for API errors. Wrap calls in try/catch to handle failures gracefully.

import { SyncClient, SyncError } from "@sync.so/sdk";
const sync = new SyncClient();
// Create the key once per action, before the first attempt, and reuse it on retries.
async function createGeneration(idempotencyKey: string) {
try {
const response = await sync.generations.create(
{
input: [
{ type: "video", url: "https://your-cdn.com/video.mp4" },
{ type: "audio", url: "https://your-cdn.com/audio.wav" },
],
model: "lipsync-2",
},
{ headers: { "Idempotency-Key": idempotencyKey } },
);
return response.id;
} catch (err) {
if (err instanceof SyncError) {
console.error(`API error ${err.statusCode}: ${JSON.stringify(err.body)}`);
// The same key makes this retry safe: it replays the original generation.
if (err.statusCode !== undefined && err.statusCode >= 500) {
console.log("Retrying in 5 seconds...");
await new Promise((r) => setTimeout(r, 5000));
return createGeneration(idempotencyKey);
}
}
throw err;
}
}

Every method takes request options as its last argument: headers, timeoutInSeconds, maxRetries and abortSignal. The client retries 408, 429 and 5xx responses twice by default; send an Idempotency-Key so those retries cannot create a second generation.

Common error scenarios:

Status CodeCauseAction
401Invalid or missing API keyCheck SYNC_API_KEY is set correctly
400Invalid input (bad URL, unsupported format)Validate inputs before sending
429Rate limit exceededBack off and retry after delay
500+Transient server errorRetry with exponential backoff

See the Error Handling guide for the full list of error codes.

TypeScript Types

The SDK is fully typed. Import types directly for use in your own functions and interfaces:

import { SyncClient } from "@sync.so/sdk";
import type { Sync } from "@sync.so/sdk";
const sync = new SyncClient();
async function processGeneration(jobId: string): Promise<void> {
const generation = await sync.generations.get(jobId);
// generation is fully typed -- your editor provides autocomplete
// for properties like .status, .outputUrl, .model, .id, etc.
if (generation.status === "COMPLETED") {
await downloadVideo(generation.outputUrl!);
}
}

The SDK exports types for all request and response objects, so you get autocomplete and compile-time checks across your entire codebase.

Framework Integration

The Sync Labs SDK is server-side only. Never import it in client-side code — doing so would expose your API key.

Next.js (App Router)

Use the SDK in Server Components, Route Handlers, or Server Actions:

app/api/generate/route.ts
import { NextResponse } from "next/server";
import { SyncClient } from "@sync.so/sdk";
const sync = new SyncClient();
export async function POST(request: Request) {
const { videoUrl, audioUrl } = await request.json();
const response = await sync.generations.create({
input: [
{ type: "video", url: videoUrl },
{ type: "audio", url: audioUrl },
],
model: "lipsync-2",
webhookUrl: "https://your-app.com/api/sync-webhook",
});
return NextResponse.json({ jobId: response.id });
}

Express

server.ts
import express from "express";
import { SyncClient } from "@sync.so/sdk";
const app = express();
const sync = new SyncClient();
app.use(express.json());
app.post("/generate", async (req, res) => {
const { videoUrl, audioUrl } = req.body;
const response = await sync.generations.create({
input: [
{ type: "video", url: videoUrl },
{ type: "audio", url: audioUrl },
],
model: "lipsync-2",
});
res.json({ jobId: response.id });
});
app.listen(3000);

Store your SYNC_API_KEY in environment variables or a secrets manager. Do not hardcode it in your source files.

Next Steps

Frequently Asked Questions

The Sync Labs TypeScript SDK requires Node.js 18 or later. It ships with both ESM and CommonJS builds, so it works with any module system. The SDK is designed for server-side use only — never import it in client-side code as this would expose your API key.

No. The SDK is server-side only. Using it in the browser would expose your API key to end users. Instead, create a server-side endpoint that calls the Sync Labs API and have your frontend communicate with your server. See the Framework Integration section for examples.

The SDK throws SyncError for API errors. Wrap your calls in try/catch blocks and inspect the statusCode and body properties. For generation creation, send an Idempotency-Key on the first attempt and preserve it and the inputs across retries. Use backoff for explicit 429 rejections; do not blindly repeat an unkeyed create after a timeout or 5xx response. Check the Error Handling guide for the full list of error codes.