GoodTake AI Docs
Character Swap API

Character Swap Overview

Replace the person in a video with your character, keeping camera, motion, timing and audio

Overview

Character Swap puts a character created with the Character API into a customer-supplied video. You send an Instagram, TikTok, Threads or YouTube Shorts link — or a direct video URL — plus a character_id, and the person in the video is replaced by your character while the camera, motion, timing and (by default) the original audio are preserved.

Base URL: https://api.goodtake.ai/v1

No prompt, no model, no reference config in the request. The swap is compiled server-side and dispatched through the same generation pipeline as everything else. The model is pinned to Dreamina Seedance 2.5 (dreamina-seedance-2-5-260628).

Endpoints

MethodPathPurpose
POST/character-swapCreate a swap from a video link + character. Returns 202.
GET/character-swap/{id}Poll: derived status, source metadata, finished video.
GET/character-swapList your organization's swaps (paginated).
POST/character-swap/{id}/regenerateNew run from the stored source. Returns 202.
GET/character-swap/estimateQuote a swap before creating one.
GET/character-swap/limitsSource-video constraints for the model in use.

Requirements

  • The character must have a completed sheet — Character API status ready, or at least a finished sheet image. Otherwise the create returns 422.
  • The source video has a maximum length, and it depends on the model in use — currently 4–30 seconds. The ceiling is a provider video-edit constraint, and swaps do not all run on the same provider, so read GET /character-swap/limits rather than hardcoding a number. The window is checked twice: an early advisory reject from the scraper's metadata, and an authoritative probe during ingest.
  • There is also a minimum frame size, which varies with the model for the same reason. limits reports the current one.

Limits

GET /character-swap/limits returns the constraints a source video must satisfy right now:

{
  "model": "dreamina-seedance-2-5-260628",
  "min_source_seconds": 4,
  "max_source_seconds": 30,
  "min_billable_seconds": 4,
  "depth_guided_max_seconds": 15,
  "trim_to_seconds": 0,
  "depth_guided": false,
  "resolutions": ["480p", "720p", "1080p"]
}

min_billable_seconds is a pricing floor, not a gate: a shorter source is accepted and billed as that length.

trim_to_seconds is the length a longer source is cut to, or 0 when nothing is trimmed. It is 0 on this API: trimming only happens where depth guidance runs, and depth guidance is an in-app feature. Where it is non-zero, a source longer than it is still accepted — its first trim_to_seconds are swapped, and the depth pass is kept rather than dropped to fit the extra length.

depth_guided is false on this API for the same reason, so max_source_seconds here is the model's full window. depth_guided_max_seconds is reported for reference only: it is the ceiling that would apply if depth were in use, which is half the window, because a depth track is a second reference clip the same length as your video and the provider budgets reference video as a total across clips. Check a video against these before you submit it — the create returns 202 immediately and a too-long source only surfaces as a failed swap after the video has been fetched and processed.

Supported sources

Social hosts are classified by hostname, not by path, and resolved through a per-platform resolver. Direct video URLs skip that step entirely.

Hostsource.kind
instagram.cominstagram
tiktok.com, vm.tiktok.com, vt.tiktok.comtiktok
threads.net, threads.comthreads
youtube.com, youtu.beyoutube
any other http(s) video URLdirect

Because classification is host-based, a link to a non-video path on a supported host still gets sent to that platform's resolver and comes back as a precise ingest error rather than an instant rejection.

Cost

One swap = one Seedance 2.5 video generation, charged on completion. Cost is token-based and scales with source duration and resolution — a swap bills tokens for the source clip and the generated output, so cost is roughly 2 × duration × the per-second token rate.

At current settings:

Resolution10s sourceApprox.
480p~87 credits~$2.61
720p~186 credits~$5.58
1080p~456 credits~$13.68

Quote before you create

GET /v1/character-swap/estimate?resolution=1080p&duration_seconds=10
curl -s "https://api.goodtake.ai/v1/character-swap/estimate?resolution=720p&duration_seconds=12" \
  -H "Authorization: Bearer $GOODTAKE_API_KEY" | jq .
{
  "estimated_cost": 223,
  "resolution": "720p",
  "duration_seconds": 12,
  "model": "dreamina-seedance-2-5-260628"
}

Durations under the provider's 4-second billing minimum are priced as 4s. Durations over 30s return 422.

Where the quote shows up

Momentestimated_cost
Create 202null — the source has not been ingested yet, so its duration (the main cost driver) is unknown. Carries estimated_cost_range {min, max} instead: the 4s and 30s brackets at the chosen resolution.
PollExact, once ingest has measured the source. Sits next to cost_credits (the actual charge, 0 until completion).
Regenerate 202Exact — the stored source's duration is already known.

The charge is capped at the quote. The billing dimensions carry the source duration as the requested output length, so a provider token overrun costs us margin, never you. Resolver runs and the audio processing are not billed separately.

Relationship to the Image API

Swap generations are visible in GET /image/generations — billed rows stay visible — but with an empty prompt, which is the only thing keeping the compiled prompt off the wire.

Explore