Skip to navigation

Batch Processing

Process multiple lipsync generations efficiently

The Sync Labs Batch API processes up to 500 lip sync generations in a single request, sent as a JSONL file or as a JSON array of rows. It is built for large runs of short clips, such as personalized video campaigns or content localization, where results can arrive over several hours.

Who can use the Batch API?

The Batch API requires a Scale plan or above and approval for your organization. Eligible plans are Scale, Enterprise, partner, and program plans. Access is disabled by default, including for new Scale subscriptions. Contact support to request access. Existing users also need approval after a usage and billing review.

An organization without approval gets 403 with errorCode: batch_access_not_enabled, including for dry_run. Once approved, other plans and free accounts get 402 with errorCode: plan_feature_unavailable. requiredPlan is scale, or enterprise on credit plans, because no self-serve credit plan includes batch.

Removing creation access leaves accepted batches running. You can still list batches, check their status, and download available results.

  • A batch holds 20 to 500 records by default, or your account’s own limit.
  • Each active batch holds one slot of your batch limit, which is your plan’s concurrency limit. Its records do not count toward /v2/generate concurrency.
  • Each record’s estimated output can be at most 30 seconds. A longer record fails in the results file with Input files exceed 30s duration, which is not supported in batch mode, while the rest of the batch runs.
  • Each record is billed like the generation it runs.

For a multipart request, send exactly one JSONL file in the input field (file also works). Other file fields or multiple files return 400. A 503 upload_capacity_unavailable creates no batch; retry the same request after Retry-After, or send a JSON rows body.

When to use batch, and when not to

A batch is a queued pipeline. Every record is downloaded and checked before any of them start, and results can take hours, with a target turnaround of up to 24 hours. Use it for large runs of short clips that do not need to finish right away.

It is not the way to send a few generations at once. For that:

Batch concurrency limits

When you already run as many batches as your plan’s concurrency limit, the API returns 429 Too Many Requests with errorCode: concurrency_limit_reached:

{
"statusCode": 429,
"error": "Too Many Requests",
"errorCode": "concurrency_limit_reached",
"message": "Batch concurrency limit reached. You have 3 active batches and your plan allows 3. Please wait for existing batches to complete or upgrade your plan for higher limits.",
"activeBatches": 3,
"concurrencyLimit": 3,
"retryAfterSeconds": 20
}

The response includes a Retry-After header (in seconds) and an X-Sync-Concurrency-Limit header. Rather than retrying immediately, wait retryAfterSeconds or until one of your active batches finishes.

How do I create a batch job?

1

Prepare Your Input File

Create a JSON Lines (.jsonl) file with your generation requests. Each line should contain a unique request_id, the endpoint (must be "/v2/generate"), and a payload with the standard generation request format (same as the Generate API). The file must be in JSON Lines (.jsonl) format with a maximum file size of 5MB, and hold 20 to 500 records by default.

input.jsonl
{"request_id": "request-1", "endpoint": "/v2/generate", "payload": {"model": "lipsync-2", "input": [{"type": "video", "url": "https://assets.sync.so/docs/example-video.mp4"}, {"type": "audio", "url": "https://assets.sync.so/docs/example-audio.wav"}]}}
{"request_id": "request-2", "endpoint": "/v2/generate", "payload": {"model": "lipsync-2", "input": [{"type": "video", "url": "https://assets.sync.so/docs/example-video.mp4"}, {"type": "audio", "url": "https://assets.sync.so/docs/example-audio.wav"}]}}
2

Create a Batch

from sync import Sync
sync = Sync()
batch = sync.batch.create(
input=open("input.jsonl", "rb")
)
print(f"Batch created with ID: {batch.id}")

Instead of uploading a file, you can send application/json with the records in rows. Each row is the same object as one JSONL line, and both forms are validated the same way: the record limits above, 5 MB or smaller when written as JSONL, and unique request_id values. When a record is refused, the error names its line. Dialogue edit refusals return it in a lineNumber field; other record errors, such as invalid JSON or a missing request_id, give it only in message (for example Record at line 4: payload is required). Lines count non-empty records, not physical lines, so blank lines in a JSONL file are skipped; with a JSON body, line N is rows[N-1]. The examples above show two records for brevity; a batch needs at least 20.

Optional parameters:

  • webhook_url: Receive notifications when the batch completes
  • dry_run: Set to true to validate your input without processing. A valid input returns {"status": "VALIDATED"} and creates no batch.

Batch creation is not covered by idempotent requests. If a create request times out, list your batches with GET /v2/batch before submitting again, so you do not run the same rows twice.

3

Check Batch Status

Monitor your batch progress by polling the status:

batch = sync.batch.get(batch_id)
print(f"Status: {batch.status}")
print(f"Progress: {batch.metrics}")

A batch can have one of the following status:

  1. PENDING: Batch created, waiting to start processing
  2. PROCESSING: Generations are being processed
  3. COMPLETED: All generations finished (successfully or with failures)
  4. FAILED: Batch processing failed entirely
4

Check Batch Results

When a batch completes, results are available as a JSON Lines file at the outputUrl of the get batch response. A GET to that URL returns a 302 redirect to a signed result file; clients that follow redirects automatically can read the JSONL response directly. Each line contains:

output.jsonl
{"request_id": "request-1", "endpoint": "/v2/generate", "payload": {...}, "status": "COMPLETED", "error": null, "response": {...}, "updated_at": "2024-01-15T10:35:00Z"}
{"request_id": "request-2", "endpoint": "/v2/generate", "payload": {...}, "status": "FAILED", "error": {"code": "INVALID_INPUT", "message": "..."}, "response": null, "updated_at": "2024-01-15T10:35:00Z"}

The response field contains the same data as individual Generate API responses. Field names in the results file are snake_case, so a completed row’s video is at response.output_url and its generation ID at response.id. Match each line to your input by request_id.

For a complete working example, see the batch processing example in our examples repository.

Webhook Notifications

When you provide a webhook_url, you’ll receive a POST notification when your batch completes or fails. It is signed with a Sync-Signature header like generation webhooks and follows the same retry policy: up to 6 attempts over about 2 hours, with the same Sync-Delivery-Id header on every attempt.

Unlike the Get Batch response, the webhook payload uses camelCase field names:

webhook_payload.json
{
"id": "batch_abc123",
"createdAt": "2024-01-15T10:30:00Z",
"status": "COMPLETED",
"webhookUrl": "https://your-webhook-url.com/batch-webhook",
"metrics": {
"totalGenerations": 5,
"successCount": 4,
"failedCount": 1,
"pendingCount": 0
},
"options": {},
"outputUrl": "https://api.sync.so/v2/batches/batch_abc123/result"
}

The completion webhook can be delivered more than once for the same batch. Use the Sync-Delivery-Id header or the batch id to ignore notifications you have already processed.

Dialogue edit rows (early access)

A batch row can render a completed dialogue edit preview instead of pairing a video with new audio. Use this to turn one recorded video into many variants, such as one per recipient name. Personalized Video Messaging has a complete script.

Dialogue edit rows are in early access and are enabled per organization. Contact us to turn them on. Until then, rows that carry dialogueEdit are refused at submit.

Each row’s payload contains:

  • model, for example sync-3.
  • input with exactly one video: the same source video the preview was made from.
  • dialogueEdit: {"id": "..."}: the ID of a completed preview from POST /v2/dialogue-edits in your organization.

Do not add audio or text inputs, segments, or dubParams; the preview supplies the audio.

{
"rows": [
{
"request_id": "line-2",
"endpoint": "/v2/generate",
"payload": {
"model": "sync-3",
"input": [{"type": "video", "url": "https://assets.sync.so/docs/example-video.mp4"}],
"dialogueEdit": {"id": "7644754c-bdfc-4d51-a78f-e0cb0cbbd40f"}
}
}
],
"dry_run": true
}

The same row objects work as lines in a JSONL file upload.

At submit, each dialogue edit row is expanded into exactly the request POST /v2/generate builds for that preview, and it is charged the same as that generation. The payload in the results file shows the expanded request (the video, the preview audio, and the edited segments), not the row you sent. Send the batch with dry_run: true first: it runs every check below, including the expansion, without rendering anything.

  • All dialogue edit rows in one batch must use previews made from the same source video. For several videos, submit one batch per video.
  • The usual batch limits apply: 20 to 500 records by default and 5 MB. Expansion adds the preview audio and edited segments to each row, and the 5 MB limit applies to the expanded batch too. A batch under 5 MB as sent can still be refused with a 400 file size error, and a dry run reports it too. If that happens, split the rows into smaller batches.
  • Most previews render as edited segments of the video. A preview that can only be rendered as whole-video lip sync is accepted in a batch only when that render is 30 seconds or shorter; render longer ones with POST /v2/generate.

Dialogue edit refusals

If any dialogue edit row cannot be rendered, the whole request is refused and no batch is created. The response carries the errorCode that POST /v2/generate would return for that preview, plus the lineNumber of the first failing row:

{
"statusCode": 422,
"errorCode": "dialogue_edit_not_found",
"lineNumber": 3,
"message": "Record at line 3: The dialogue edit was not found in this organization."
}
errorCodeStatusCauseFix
dialogue_edit_not_found422The ID is unknown or belongs to another organization.Use a preview your organization created.
dialogue_edit_not_completed422The preview is still PENDING or PROCESSING, or it FAILED.While it is pending or processing, poll GET /v2/dialogue-edits/{id} until it completes, then submit. A FAILED preview is terminal: create a new one.
dialogue_edit_source_mismatch422The row’s video is not the video the preview was made from.Send the preview’s original source video.
dialogue_edit_retime_required422The preview lacks the retiming it needs to render (segmentLipsyncEnabled or sectionExpansionEnabled is false on it).Create a new preview. If it still reports false, contact us.
dialogue_edit_removal_too_large422The edit removes more speech than the surrounding footage can absorb.Restore some words and create a new preview.
dialogue_edit_audio_conflict422The row also has an audio or text input, segments, or dubParams.Send only the video input and dialogueEdit.
dialogue_edit_unsupported422The API cannot render this preview, including a whole-video render longer than 30 seconds.Create a new whole-video preview with an API key, or render this one with POST /v2/generate.
dialogue_edit_batch_mixed_sources422The previews in the batch come from different source videos.Submit one batch per source video.
generation_input_validation_failed400dialogueEdit is not an object with a UUID id, or input does not hold exactly one video.Fix the row’s shape.

Only the first failing row is reported. Fix it and repeat the dry run until it returns {"status": "VALIDATED"}.

  • Rate Limits — understand API rate limiting to plan your batch sizes effectively
  • Webhooks Guide — set up webhook notifications for batch completion events
  • Error Handling — reference for batch-specific error codes and troubleshooting