Create a Swap
POST /v1/character-swap — swap your character into a video
Endpoint
POST /v1/character-swapReturns 202 Accepted immediately. Fetching the video, probing its
duration, extracting its audio and mirroring it all happen in the background —
so every ingest failure surfaces on the swap resource, not on this
response.
Request body
{
"character_id": "9d2f6a3e-4c11-4a90-b1d7-2f8e6a4c0b11",
"video_url": "https://www.instagram.com/reel/ABC123/",
"background": "a neon-lit rooftop at night",
"with_audio": true,
"resolution": "1080p"
}| Field | Type | Required | Description |
|---|---|---|---|
character_id | string | Yes | A character with a completed sheet. Otherwise 422. |
video_url | string | Yes | A social post link or a direct video URL. Must resolve to a 4–30 second video. Internal/private addresses are rejected. |
background | string | No | Optional environment replacement. Sanitized server-side before it can reach the prompt (600-char cap). Omitted = the original environment is kept. |
with_audio | boolean | No | Default true. Keeps the source video's original audio track. |
resolution | string | No | 480p | 720p | 1080p. Drives the cost. |
Response — 202 Accepted
{
"id": "1f7b2c40-8e55-4b21-9f03-77a1d0c4e902",
"status": "pending",
"character_id": "9d2f6a3e-4c11-4a90-b1d7-2f8e6a4c0b11",
"model": "dreamina-seedance-2-5-260628",
"estimated_cost": null,
"estimated_cost_range": { "min": 183, "max": 1368 },
"with_audio": true,
"resolution": "1080p",
"poll_url": "/public/v1/character-swap/1f7b2c40-8e55-4b21-9f03-77a1d0c4e902"
}estimated_cost is null here on purpose: the source has not been ingested
yet, so its duration — the main cost driver — is not yet known. Poll the swap
for the exact figure.
How audio works
with_audio: true (the default) extracts the original audio track during
ingest and muxes it onto the generated video before the video is stored, so
you never see a silent intermediate. The model is always asked for a silent
video and the original track is layered back on.
with_audio: false delivers the silent result.
If the audio mux fails for any reason, you get the silent video rather than a failed generation. A paid generation is never thrown away over an audio step.
Content-safety rejections are terminal
The character sheet and the source video are both moderated before any generation is submitted. A moderation verdict is deterministic — the generation API would refuse the identical bytes for the identical reason moments later — so the swap stops there, nothing is dispatched, and nothing is billed.
Which input was refused is part of the contract, because the fix is different in each case:
The source video was refused (most often: it shows an identifiable real person):
{
"status": "failed",
"error": "the source video was rejected by the content-safety filter (it may show an identifiable real person); try a different video"
}The character sheet was refused — the video is fine, and changing it accomplishes nothing:
{
"status": "failed",
"error": "the character sheet was rejected by the content-safety filter; recreate the character from different reference photos"
}Regenerate cannot help in either case — the stored material is permanently
unusable — and answers 422 with the same message. Create a new swap from a
different video, or a new character from different photos.
Example
curl -s -X POST https://api.goodtake.ai/v1/character-swap \
-H "Authorization: Bearer $GOODTAKE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"character_id": "9d2f6a3e-4c11-4a90-b1d7-2f8e6a4c0b11",
"video_url": "https://www.tiktok.com/@someone/video/7300000000000000000",
"resolution": "720p",
"with_audio": true
}' | jq -r .idErrors
| Status | Meaning |
|---|---|
400 | Invalid request, or a video_url rejected by the URL guard |
401 | Missing or invalid API key |
404 | Unknown character_id, or one belonging to another organization |
422 | The character has no completed sheet |
503 | The pinned model is disabled, or social-link ingestion is not configured on this deployment (direct video URLs keep working) |