TypeScript SDK Guide

The Sync TypeScript SDK (@sync.so/sdk) provides typed methods for Node.js 18+ applications. It wraps the full Sync 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

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:

1import { SyncClient } from "@sync.so/sdk";
2
3// Reads SYNC_API_KEY from environment
4const sync = new SyncClient();
5
6// Or pass it directly
7const 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
1import { SyncClient, SyncError } from "@sync.so/sdk";
2
3const sync = new SyncClient();
4
5async function main() {
6 // 1. Submit a generation
7 const response = await sync.generations.create({
8 input: [
9 { type: "video", url: "https://assets.sync.so/docs/example-video.mp4" },
10 { type: "audio", url: "https://assets.sync.so/docs/example-audio.wav" },
11 ],
12 model: "lipsync-2",
13 options: { sync_mode: "cut_off" },
14 });
15
16 const jobId = response.id;
17 console.log(`Job submitted: ${jobId}`);
18
19 // 2. Poll until complete
20 let generation = await sync.generations.get(jobId);
21 while (!["COMPLETED", "FAILED", "REJECTED"].includes(generation.status)) {
22 await new Promise((r) => setTimeout(r, 10000));
23 generation = await sync.generations.get(jobId);
24 }
25
26 // 3. Get output
27 if (generation.status === "COMPLETED") {
28 console.log(`Output: ${generation.outputUrl}`);
29 } else {
30 console.log(`Generation ${jobId} failed`);
31 }
32}
33
34main();

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 generates lip movements matching the audio.

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

Video + Text-to-Speech

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

1const response = await sync.generations.create({
2 input: [
3 {
4 type: "video",
5 url: "https://your-cdn.com/video.mp4",
6 },
7 {
8 type: "text",
9 provider: {
10 name: "elevenlabs",
11 voiceId: "EXAVITQu4vr4xnSDxMaL",
12 script: "Your script goes here. Max 5,000 characters per generation.",
13 stability: 0.5,
14 similarityBoost: 0.75,
15 },
16 },
17 ],
18 model: "lipsync-2",
19 options: { sync_mode: "cut_off" },
20});

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.

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

Listing Generations

Retrieve your recent generations:

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

Getting Generation Details

Fetch a specific generation by ID:

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

Cost Estimation

Estimate the cost of a generation before submitting it:

1const estimate = await sync.generations.estimateCost({
2 input: [
3 { type: "video", url: "https://your-cdn.com/video.mp4" },
4 { type: "audio", url: "https://your-cdn.com/audio.wav" },
5 ],
6 model: "lipsync-2",
7});
8
9console.log(`Estimated cost: ${estimate.totalCredits} credits`);

Working with Files

URL Inputs

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

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

Uploading Files via the Assets API

If your files are not hosted at public URLs, use the Assets API to upload them first. Then reference the returned URLs in your generation request.

1// 1. List your uploaded assets
2const assets = await sync.assets.list();
3
4// 2. Use an asset URL in a generation
5const response = await sync.generations.create({
6 input: [
7 { type: "video", url: assets[0].url },
8 { type: "audio", url: "https://your-cdn.com/audio.wav" },
9 ],
10 model: "lipsync-2",
11});

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.

1import { SyncClient, SyncError } from "@sync.so/sdk";
2
3const sync = new SyncClient();
4
5async function createGeneration() {
6 try {
7 const response = await sync.generations.create({
8 input: [
9 { type: "video", url: "https://your-cdn.com/video.mp4" },
10 { type: "audio", url: "https://your-cdn.com/audio.wav" },
11 ],
12 model: "lipsync-2",
13 });
14 return response.id;
15 } catch (err) {
16 if (err instanceof SyncError) {
17 console.error(`API error ${err.statusCode}: ${JSON.stringify(err.body)}`);
18
19 // Retry on transient errors (5xx)
20 if (err.statusCode >= 500) {
21 console.log("Retrying in 5 seconds...");
22 await new Promise((r) => setTimeout(r, 5000));
23 return createGeneration();
24 }
25 }
26 throw err;
27 }
28}

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:

1import { SyncClient } from "@sync.so/sdk";
2import type { Sync } from "@sync.so/sdk";
3
4const sync = new SyncClient();
5
6async function processGeneration(jobId: string): Promise<void> {
7 const generation = await sync.generations.get(jobId);
8
9 // generation is fully typed -- your editor provides autocomplete
10 // for properties like .status, .outputUrl, .model, .id, etc.
11 if (generation.status === "COMPLETED") {
12 await downloadVideo(generation.outputUrl!);
13 }
14}

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 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
1import { NextResponse } from "next/server";
2import { SyncClient } from "@sync.so/sdk";
3
4const sync = new SyncClient();
5
6export async function POST(request: Request) {
7 const { videoUrl, audioUrl } = await request.json();
8
9 const response = await sync.generations.create({
10 input: [
11 { type: "video", url: videoUrl },
12 { type: "audio", url: audioUrl },
13 ],
14 model: "lipsync-2",
15 webhookUrl: "https://your-app.com/api/sync-webhook",
16 });
17
18 return NextResponse.json({ jobId: response.id });
19}

Express

server.ts
1import express from "express";
2import { SyncClient } from "@sync.so/sdk";
3
4const app = express();
5const sync = new SyncClient();
6
7app.use(express.json());
8
9app.post("/generate", async (req, res) => {
10 const { videoUrl, audioUrl } = req.body;
11
12 const response = await sync.generations.create({
13 input: [
14 { type: "video", url: videoUrl },
15 { type: "audio", url: audioUrl },
16 ],
17 model: "lipsync-2",
18 });
19
20 res.json({ jobId: response.id });
21});
22
23app.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 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 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 transient errors like 500 or 429, implement retry logic with exponential backoff. Check the Error Handling guide for the full list of error codes.