> 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. # Asset Uploads > Upload your own video, audio, and image files to sync. labs, register them as reusable assets, and reference them by ID in lip sync generations and voice clones. Most generations reference media by public URL. When your files aren't already hosted somewhere public — or you want a reusable, plan-aware reference you can pass to multiple jobs — upload them to sync. labs as **assets** and reference them by `id`. Asset uploads are the recommended way to generate from large local files. Upload directly to storage once, then retry a generation using the asset ID without sending the file again. The application allows a single asset upload up to 5 GiB (5 × 1024³ bytes), subject to your plan's file size limit. Direct multipart uploads to `POST /v2/generate` have a 5 GiB per-file application parser limit and require a sustained upload with each request. Proxy limits may be lower; the parser limit does not guarantee a 5 GiB direct transfer. Uploading is a three-step flow: 1. **Request a presigned URL** with `POST /v2/assets/upload` (tells sync. labs the file's name, type, and size). 2. **PUT the raw file bytes** directly to that presigned URL (a plain HTTP `PUT`, no auth header). 3. **Register the asset** with `POST /v2/assets`, which verifies the upload, enforces plan limits on the actual file size, and returns an `id`. You then use the returned `id` as an `input[].assetId` in a [generation](/api-reference/api/generate-api/create) or as the `assetId` voice-clone sample in [voice cloning](/developer-guides/voice-cloning). > **Note** > > If your file is already hosted at a publicly accessible URL, you can skip the presign + PUT steps entirely and register it directly with `POST /v2/assets` — see [Register an already-public URL](#register-an-already-public-url) below. ## The upload flow #### Request a presigned URL Call `POST /v2/assets/upload` with the file's `fileName`, `contentType` (the MIME type, e.g. `video/mp4`), and `size` in bytes. You get back an `uploadUrl` to `PUT` the bytes to, the canonical `url` you'll register in step 3, and `expiresIn` (seconds until the presigned URL expires). **`curl`** ```bash curl curl -X POST https://api.sync.so/v2/assets/upload \ -H "x-api-key: $SYNC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "fileName": "speaker.mp4", "contentType": "video/mp4", "size": 8388608 }' ``` **`upload.py`** ```python upload.py from sync import Sync sync = Sync() presign = sync.assets.create_upload( file_name="speaker.mp4", content_type="video/mp4", size=8_388_608, ) print(presign.upload_url) # PUT the bytes here print(presign.url) # register this URL in step 3 ``` **`upload.ts`** ```typescript upload.ts import { SyncClient } from "@sync.so/sdk"; const sync = new SyncClient(); const presign = await sync.assets.createUpload({ fileName: "speaker.mp4", contentType: "video/mp4", size: 8_388_608, }); console.log(presign.uploadUrl); // PUT the bytes here console.log(presign.url); // register this URL in step 3 ``` A `201` response looks like: ```json { "uploadUrl": "https://uploads.sync.so/...&X-Amz-Signature=...", "url": "https://assets.sync.so/uploads/abc123/speaker.mp4", "expiresIn": 3600 } ``` #### PUT the file to the presigned URL Upload the raw file bytes with a plain HTTP `PUT` to `uploadUrl`. **Do not** send your `x-api-key` here — the URL is already signed. Treat it as a temporary credential. You **must** send the same `Content-Type` you declared in step 1, or the upload is rejected. **`curl`** ```bash curl curl --fail-with-body --upload-file speaker.mp4 \ -H "Content-Type: video/mp4" \ "$UPLOAD_URL" ``` `curl --upload-file` sends a `PUT` and streams the file from disk, so large files don't need to fit in memory. Wait for the `PUT` to finish before you register the asset. > **Warning** > > The `Content-Type` header on the `PUT` must exactly match the `contentType` you sent to `/v2/assets/upload`. A mismatch (or a missing header) causes the upload to fail signature validation. The presigned URL also expires after `expiresIn` seconds — request a fresh one if it lapses. #### Register the asset Call `POST /v2/assets` with the `url` returned in step 1 and the asset `type`. Registration verifies the upload actually landed and enforces your plan's size limits against the **actual** uploaded file size. A `201` response returns an Asset with an `id`. **`curl`** ```bash curl curl -X POST https://api.sync.so/v2/assets \ -H "x-api-key: $SYNC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://assets.sync.so/uploads/abc123/speaker.mp4", "type": "VIDEO", "name": "Speaker take 1" }' ``` **`register.py`** ```python register.py asset = sync.assets.create( url=presign.url, type="VIDEO", name="Speaker take 1", ) print(asset.id) # use this assetId in a generation ``` **`register.ts`** ```typescript register.ts const asset = await sync.assets.create({ url: presign.url, type: "VIDEO", name: "Speaker take 1", }); console.log(asset.id); // use this assetId in a generation ``` ## Request parameters ### `POST /v2/assets/upload` **`fileName`** `string` — required The name of the file you're uploading, including its extension (e.g. `speaker.mp4`). --- **`contentType`** `string` — required The file's MIME type (e.g. `video/mp4`, `audio/wav`, `image/png`). You must send this exact value as the `Content-Type` header when you `PUT` the bytes. --- **`size`** `integer` — required The file size in bytes. Single uploads are capped at **5 GiB**. --- Returns `201` with `uploadUrl` (where to `PUT` the bytes), `url` (the canonical URL to register), and `expiresIn` (seconds until the presigned URL expires). ### `POST /v2/assets` **`url`** `string` — required The `url` returned by `POST /v2/assets/upload`, or any publicly accessible URL if you're registering pre-hosted media directly. --- **`type`** `string` — required The asset type — one of `AUDIO`, `VIDEO`, or `IMAGE`. --- **`name`** `string` An optional human-readable label for the asset. --- **`projectId`** `string` An optional project to associate the asset with. --- Returns `201` with the registered Asset, including its `id`. ## Using an assetId in a generation Once you have an asset `id`, reference it from a generation's `input` array with `assetId` instead of `url`. Mix asset-backed and URL-backed inputs freely in the same request. **`curl`** ```bash curl curl -X POST https://api.sync.so/v2/generate \ -H "x-api-key: $SYNC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "lipsync-2", "input": [ { "type": "video", "assetId": "asset_abc123" }, { "type": "audio", "url": "https://assets.sync.so/docs/example-audio.wav" } ], "options": { "sync_mode": "cut_off" } }' ``` **`generate.py`** ```python generate.py from sync import Sync from sync.common import Audio, Video, GenerationOptions sync = Sync() response = sync.generations.create( input=[ Video(asset_id="asset_abc123"), Audio(url="https://assets.sync.so/docs/example-audio.wav"), ], model="lipsync-2", options=GenerationOptions(sync_mode="cut_off"), ) print(response.id) ``` **`generate.ts`** ```typescript generate.ts import { SyncClient } from "@sync.so/sdk"; const sync = new SyncClient(); const response = await sync.generations.create({ input: [ { type: "video", assetId: "asset_abc123" }, { type: "audio", url: "https://assets.sync.so/docs/example-audio.wav" }, ], model: "lipsync-2", options: { sync_mode: "cut_off" }, }); console.log(response.id); ``` To retry a generation, resend the same request body with the same [`Idempotency-Key`](/api-reference/guides/idempotency) header. Don't repeat the upload or registration steps; the asset `id` stays valid, and you can reuse it in later generations. You can also pass an asset `id` as the voice-clone sample (`assetId`) when creating a cloned voice with `POST /v2/voices` — see [Voice Cloning](/developer-guides/voice-cloning). ## Register an already-public URL If your media is already hosted at a publicly accessible URL, skip the presign and `PUT` steps and register it directly. sync. labs fetches the file to verify it and enforce plan limits. **`curl`** ```bash curl curl -X POST https://api.sync.so/v2/assets \ -H "x-api-key: $SYNC_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-cdn.com/clips/speaker.mp4", "type": "VIDEO", "name": "Speaker take 1" }' ``` **`register_public.py`** ```python register_public.py asset = sync.assets.create( url="https://your-cdn.com/clips/speaker.mp4", type="VIDEO", name="Speaker take 1", ) print(asset.id) ``` **`registerPublic.ts`** ```typescript registerPublic.ts const asset = await sync.assets.create({ url: "https://your-cdn.com/clips/speaker.mp4", type: "VIDEO", name: "Speaker take 1", }); console.log(asset.id); ``` ## Limits and rate limits | | | | :------------------------- | :---------------------------------------------------------------------------------------------------------- | | **Maximum single upload** | 5 GiB per file | | **Presign rate limit** | 120 requests/minute per API key on `POST /v2/assets/upload` | | **Size enforcement** | Plan size limits are checked at registration against the actual uploaded file size, not the declared `size` | | **Presigned URL lifetime** | `expiresIn` seconds (from the `/upload` response); request a new one if it expires | ## Frequently asked questions #### Why upload an asset instead of just passing a URL? If your media isn't already hosted at a publicly accessible URL, uploading is the way to get it into a generation. Assets are also reusable — register once, then reference the same `id` across many generations or voice clones without re-uploading or re-hosting the file. Registration also validates the file and enforces your plan's size limits up front, so you catch oversized or unreachable media before submitting a generation. #### My PUT to the presigned URL is failing. What's wrong? The two most common causes are a `Content-Type` mismatch and an expired URL. The `Content-Type` header on your `PUT` must exactly match the `contentType` you sent to `POST /v2/assets/upload` — if you declared `video/mp4`, you must `PUT` with `Content-Type: video/mp4`. Also confirm you are **not** sending your `x-api-key` header on the `PUT`; the presigned URL carries its own signature and adding auth headers can break it. Finally, presigned URLs expire after `expiresIn` seconds — if too much time has passed, request a fresh one and retry. #### When are plan size limits enforced? At registration (`POST /v2/assets`), not at presign. The `/upload` step only generates a signed URL — sync. labs checks the **actual** uploaded file size against your plan limits when you register the asset, so a successful presign and `PUT` does not guarantee the file is within your plan's allowance. ## Related * [Voice Cloning](/developer-guides/voice-cloning) — use an uploaded `assetId` as the sample for `POST /v2/voices`. * [Generate API](/api-reference/api/generate-api/create) — reference an asset as `input[].assetId` in a lip sync generation. * [Media formats support](/compatibility-and-tips/media-formats-support) — supported file types and codecs for video, audio, and image assets. > Upload your own video, audio, and image files to sync. labs, register them as reusable assets, and reference them by ID in lip sync generations and voice clones.