POST /api/v2/recast
Keeps a clip’s motion, camera and timing and swaps who or what is in it. The price follows the clip’s real length, read from the file. Needs the generate scope.
- Auth: API key (Authorization: Bearer)
- Key scope needed:
generate - Raw spec: https://app.diffusr.ai/api/v2/openapi.json
Headers
| Name | Type | Required | Description |
|---|---|---|---|
Idempotency-Key | string | no | 1-255 visible ASCII characters you make up for this request (a UUID is fine). Send the same key to retry safely: you get the first job back instead of paying twice. Reusing a key for a different request answers 409 IDEMPOTENCY_CONFLICT. Wins over idempotency_key in the body. |
Request body (JSON)
| Name | Type | Required | Description |
|---|---|---|---|
video_url | string | yes | The source clip, an MP4 or MOV link (upload_asset gives one). Its motion, camera and timing are kept. At most 10 s on the default model (30 s on seedance-2-5): a longer clip is refused before anything is spent, so trim it first. Its real length is read from the file and is what you are priced on. |
image_urls | string[] | no | Photos of who or what should appear in the clip. |
mode | “world” | “swap” | no | “world” = same motion, new people, place and look from the photos. “swap” = replace the main subject with the first photo, keep the rest. Use this OR prompt. |
prompt | string | no | Your own instruction instead of a mode. Say the change, then what to keep. |
duration_sec | integer | no | Ignored. The output length follows your clip’s real length, read from video_url (rounded, within the model’s range). It is also what you are priced on. |
aspect_ratio | string | no | e.g. “9:16”, “16:9”, “1:1”. Leave out for the model’s default. |
resolution | string | no | e.g. “720p”. Leave out for the model’s default. |
model | string | no | Leave out for the recommended model. Ids come from list_models. |
dry_run | boolean | no | true = only check the request and return the price in credits. Nothing is made, nothing is spent. |
idempotency_key | string | no | A unique string you make up for this request. Retry with the same key and you get the first result back instead of paying again. |
callback_url | string | no | Optional. A public https:// link. When the job finishes (done, failed or canceled) we POST the finished result here, signed. See the docs for checking the signature. Polling get_result still works. |
Example
curl -X POST https://app.diffusr.ai/api/v2/recast \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "video_url": "https://example.com/clip.mp4", "mode": "swap", "image_urls": [ "https://example.com/new-person.jpg" ]}'import { Diffusr } from "diffusr";
const d = new Diffusr(); // reads DIFFUSR_KEYconst job = await d.recast({ video_url: "https://example.com/clip.mp4", mode: "swap", image_urls: [ "https://example.com/new-person.jpg", ],});const result = await d.waitFor(job.id);console.log(result.output?.url);from diffusr import Diffusr
d = Diffusr() # reads DIFFUSR_KEYjob = d.recast( video_url="https://example.com/clip.mp4", mode="swap", image_urls=[ "https://example.com/new-person.jpg", ],)result = d.wait_for(job["id"])print(result["output"]["url"])The same call as a dry run (the price, nothing made, nothing spent):
curl -X POST https://app.diffusr.ai/api/v2/recast \ -H "Authorization: Bearer $DIFFUSR_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "video_url": "https://example.com/clip.mp4", "mode": "swap", "image_urls": [ "https://example.com/new-person.jpg" ], "dry_run": true}'import { Diffusr } from "diffusr";
const d = new Diffusr(); // reads DIFFUSR_KEYconst job = await d.recast({ video_url: "https://example.com/clip.mp4", mode: "swap", image_urls: [ "https://example.com/new-person.jpg", ], dry_run: true,});console.log(job); // the price, nothing was madefrom diffusr import Diffusr
d = Diffusr() # reads DIFFUSR_KEYjob = d.recast( video_url="https://example.com/clip.mp4", mode="swap", image_urls=[ "https://example.com/new-person.jpg", ], dry_run=True,)print(job) # the price, nothing was madeResponse
200 ApiDryRun
dry_run was true: the price, nothing made, nothing spent.
| Name | Type | Required | Description |
|---|---|---|---|
dry_run | true | yes | |
test | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. |
credits_estimated | number | yes | |
price | object | no | |
price.credits | number | yes | The most this call costs, in whole credits (1 credit = $0.01). |
price.usd | string | yes | The same price in US dollars, e.g. “0.0336”. |
price.tariff | “direct” | “outcome” | yes | “direct” = your prompt goes to the model as sent; “outcome” = a result we shape (Recast, creator video). |
price.tariff_version | string | yes | |
price.expires_at | string | yes | The quoted price holds until then (ISO time). |
duration_sec | number | no | |
warnings | string[] | no | |
note | string | yes |
202 ApiStarted
The job started. Poll GET /results/{id}.
| Name | Type | Required | Description |
|---|---|---|---|
id | string | yes | The job id. Poll GET /results/{id} with it, or wait for the callback. |
status | string | yes | “PENDING” on a new start; the run’s current status when a retried Idempotency-Key replays the first job. |
credits_estimated | number | yes | The most this job can cost, in credits. |
idempotent_replay | true | no | true when this is the FIRST job again, answering a retried Idempotency-Key. |
test | true | no | true when a test key made the call: the price is real, nothing real ran. Absent for live keys. |
price | object | no | The price, locked in at start. |
price.credits | number | yes | The most this call costs, in whole credits (1 credit = $0.01). |
price.usd | string | yes | The same price in US dollars, e.g. “0.0336”. |
price.tariff | “direct” | “outcome” | yes | “direct” = your prompt goes to the model as sent; “outcome” = a result we shape (Recast, creator video). |
price.tariff_version | string | yes | |
price.expires_at | string | yes | The quoted price holds until then (ISO time). |
duration_sec | number | no | |
warnings | string[] | no | Heads-ups that did not stop the call. |
next | string | yes | What to do next, in words. |
Errors
Every error body is { error, code, request_id, ... }. All codes and what to do.
- 401: Missing, wrong, revoked or expired API key (code UNAUTHORIZED, KEY_REVOKED, KEY_EXPIRED).
- 402: Not enough credits, or an API budget would be passed (INSUFFICIENT_CREDITS, BUDGET_EXCEEDED).
- 403: The key may not do this (SCOPE_DENIED, MODEL_NOT_ALLOWED, TEST_MODE_UNSUPPORTED, ACCOUNT_BLOCKED, allowance_exceeded).
- 409: IDEMPOTENCY_CONFLICT or CALLBACK_NOT_RETRYABLE.
- 413: The request or file is too large (TOO_LARGE, REFERENCE_TOO_LARGE).
- 422: The request is not valid (VALIDATION_FAILED, INVALID_JSON, PREFLIGHT_FAILED, CALLBACK_URL_INVALID, REFERENCE_*). `issues` lists each problem. Nothing was charged.
- 429: Too many requests or jobs (RATE_LIMITED, CONCURRENCY_LIMIT, UPLOAD_QUOTA, …). Wait Retry-After seconds, then retry.
- 500: Our fault (INTERNAL_ERROR). Quote request_id to support.
- 503: Paused or busy (API_PAUSED, MODEL_PAUSED, PROVIDER_PAUSED, PROVIDER_BUSY, UNAVAILABLE). Wait Retry-After seconds, then retry.
Charges
Costs credits (1 credit = $0.01). Send `dry_run: true` first: it answers with the price and spends nothing. You are charged when the result is delivered and never more than the quoted price. Work that fails, is canceled before it runs, or is never delivered is not charged. A test key quotes the same price and charges nothing. It is priced at the outcome rate: a result we shape for you, so the rate is higher than the model prices on the Prices page. Use `dry_run: true` for the exact price.