Genjutsu
Generate a new video from a source video and reference images.
mode | What it does |
|---|---|
motion_control | Motion transfer: the person in your reference images performs the movements from the source video. |
replace_object | Object replacement: an object in the source video is replaced with the one in your reference images. |
Create a task
POST https://api.nanoapi.ai/v1/genjutsuReads the source video, freezes the cost from your available balance and returns 202 Accepted with a queued task. The video is generated in the background, usually in 10–20 minutes; get it by webhook or with GET /v1/tasks/{id}. Tasks can't be cancelled.
Reading the source video takes 1–3 seconds, at most about 35: set your client timeout to 60 seconds or more.
| Parameter | Type | Required | Description |
|---|---|---|---|
model | string | No | Model version: genjutsu-1.0 (the default, and the only one for now). Available versions are in GET /v1/products (models). Set it explicitly: when a new version ships, requests without model keep using genjutsu-1.0. An unknown version returns 400 invalid_request with param: "model". |
mode | string | Yes | motion_control or replace_object. |
video_url | string | Yes | Source video. See Media. |
image_urls | string[] | Yes | 1–10 reference images. See Media. |
prompt | string | No | At most 5000 characters. |
resolution | string | No | 480p, 720p (default) or 1080p. Sets the price. |
num_outputs | integer | No | Only 1 for now (the default). |
metadata | object | No | Your own key-value pairs, returned on the task and in webhooks: up to 16 keys of 1–40 characters, string values up to 500 characters. Put your order ID here to find the task later. |
webhook_url | string | No | Notified when the task succeeds or fails: https, up to 2048 characters. See Webhooks. |
curl https://api.nanoapi.ai/v1/genjutsu \
-H "Authorization: Bearer $NANOAPI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "genjutsu-1.0",
"mode": "motion_control",
"image_urls": ["https://example.com/person.png"],
"video_url": "https://example.com/dance.mp4",
"resolution": "720p",
"metadata": { "order_id": "A1001" },
"webhook_url": "https://example.com/hooks/nanoapi"
}'Response (abbreviated; every field is described in The task object):
{
"object": "task",
"id": "tk_01jb8z5q4w3e2r1t0y9u8i7o6p",
"product": "genjutsu",
"model": "genjutsu-1.0",
"status": "queued",
"billing": { "currency": "USD", "frozen": "4.095", "charged": "0.00", "refunded": "0.00", "billed_seconds": null },
"metadata": { "order_id": "A1001" },
"created_at": "2026-10-22T08:30:00Z"
}Media
All media is passed as public http(s) URLs. There's no upload endpoint: host the files yourself, for example in your own object storage. URLs are at most 4096 characters and can't point to localhost or private, loopback or reserved addresses.
Source video (video_url) | Reference images (image_urls) | |
|---|---|---|
| Count | 1 | 1–10 |
| Format | A direct download in mp4, mov, webm or mkv | Common image formats |
| Length | At most 30 seconds; 4–30 seconds for replace_object | – |
| When it's read | When you create the task, to calculate the price | When generation runs |
| If it can't be used | The request fails with 400 (see Errors); no task, nothing frozen | The task fails and the frozen amount is returned in full |
Keep every link reachable until the task finishes, including links that expire.
Pricing
cost = billed seconds of the source video × price per second of the resolution| Resolution | Price per second | 30-second video |
|---|---|---|
| 480p | $0.117 | $3.51 |
| 720p | $0.273 | $8.19 |
| 1080p | $0.78 | $23.40 |
Billed seconds:
- Length: the longer of the video and audio tracks, not the duration in the file's metadata. A few tens of milliseconds of encoding error are ignored (a video that's exactly 30 s but reports 30.02 s is accepted).
motion_control: rounded to the nearest second, at least 4 seconds.replace_object: rounded up to a whole second.
Example: a 15-second video at 720p freezes $4.095 and is charged $4.095 when it succeeds. Prices can change: read them from GET /v1/products instead of hard-coding them. How freezing and charging work in general: Billing.
To show a price before submitting, read the video's length on your side and apply the rules above. It's an estimate: billing.frozen in the response is what counts.
Errors
Errors specific to this endpoint. All codes and how to handle them: Errors.
| Status | code | When |
|---|---|---|
| 400 | invalid_media | The source video can't be read: the link doesn't open or has expired, isn't public, or isn't a supported video. details.reason says why. |
| 400 | video_too_long | The source video is longer than 30 seconds (details.video_duration). |
| 400 | video_too_short | The replace_object source video is shorter than 4 seconds. |
| 402 | insufficient_balance | Your available balance can't cover the cost (details.required, details.available). |
| 429 | too_many_inflight_tasks | You have 10 tasks queued or running (details.limit). |
| 503 | service_unavailable | The price can't be calculated right now. No task was created; retry after Retry-After seconds. |
None of these create a task or freeze anything.