Asset Uploads

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.

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 or as the assetId voice-clone sample in voice cloning.

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 below.

The upload flow

1

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 -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
> }'

A 201 response looks like:

1{
2 "uploadUrl": "https://uploads.sync.so/...&X-Amz-Signature=...",
3 "url": "https://assets.sync.so/uploads/abc123/speaker.mp4",
4 "expiresIn": 3600
5}
2

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. You must send the same Content-Type you declared in step 1, or the upload is rejected.

curl
$curl -X PUT "$UPLOAD_URL" \
> -H "Content-Type: video/mp4" \
> --data-binary @speaker.mp4

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.

3

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 -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"
> }'

Request parameters

POST /v2/assets/upload

fileName
stringRequired

The name of the file you’re uploading, including its extension (e.g. speaker.mp4).

contentType
stringRequired

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
integerRequired

The file size in bytes. Single uploads are capped at 5 GB.

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
stringRequired

The url returned by POST /v2/assets/upload, or any publicly accessible URL if you’re registering pre-hosted media directly.

type
stringRequired

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 -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" }
> }'

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.

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 -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"
> }'

Limits and rate limits

Maximum single upload5 GB per file
Presign rate limit120 requests/minute per API key on POST /v2/assets/upload
Size enforcementPlan size limits are checked at registration against the actual uploaded file size, not the declared size
Presigned URL lifetimeexpiresIn seconds (from the /upload response); request a new one if it expires

Frequently asked questions

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.

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.

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.

  • Voice Cloning — use an uploaded assetId as the sample for POST /v2/voices.
  • Generate API — reference an asset as input[].assetId in a lip sync generation.
  • Media formats support — supported file types and codecs for video, audio, and image assets.