Wan 3.0 API: How to Generate and Extend AI Videos

A useful video shot can end before the action does: the camera is still moving toward a product, or the subject has not finished crossing the scene. With the Wan 3.0 API, you can start a new clip from a prompt or image, or ask for a continuation using an existing short video as a reference.
Through Tokenhot, these requests use POST https://api.tokenhot.ai/v1/video/generations. What changes is the input: text for a new scene, frame images for a controlled start or ending, and reference_video for a continuation. Extension also requires the adaptive aspect ratio and a duration that fits the source clip.
This guide follows Tokenhot's documentation checked on October 26, 2026. The examples were checked offline against the published specifications; no video generation, quality comparison, or billing test was performed.
Start with the material you already have
Choose the input that gives you the control your shot needs:
| Your starting point | Use it for | Request input |
|---|---|---|
| A written scene | Generate a new clip without a reference asset | input.prompt |
| A starting image, optionally an ending image | Describe motion between defined frame positions | input.media with first_frame, optionally last_frame |
| Images or clips to refer to | Describe a new shot using reference material | reference_image or reference_video |
| A short clip you want to continue | Ask for a continuation of its action | reference_video, a continuation prompt, and parameters.ratio: "adaptive" |
The Wan creation reference documents both wan3.0-video and wan3.0-video-prime. This tutorial uses the standard wan3.0-video ID throughout. Evaluate a different route against your account's current conditions rather than assuming the same price or turnaround time.
For a first integration, a short text-only clip lets you check submission, task lookup, and saving before adding media hosting to the job. If your actual task starts from a product image or an existing video, use the corresponding input below instead of generating an unnecessary preliminary clip.
Generate your first clip
You need an authorized Tokenhot account and API key, access to this route, Bash or a compatible shell, a recent cURL, and Python 3. The Quick Start explains the documented API setup. Keep the key in your local TOKENHOT_API_KEY environment variable.
Use a new private working folder for each submission, and run the following commands from that folder. The files belong to one job; preserve them if a request fails.
Save this authored example as request.json:
{
"model": "wan3.0-video",
"input": {
"prompt": "A slow camera move toward a white ceramic lamp on a wooden desk. Soft afternoon light, a still background, and one continuous shot."
},
"parameters": {
"resolution": "720P",
"ratio": "16:9",
"duration": 5,
"audio": false
}
}
Submit it once:
curl --silent --show-error --fail-with-body --max-time 120 \
--request POST 'https://api.tokenhot.ai/v1/video/generations' \
--header "Authorization: Bearer ${TOKENHOT_API_KEY:?Set TOKENHOT_API_KEY first}" \
--header 'Content-Type: application/json' \
--data-binary @request.json \
--output submission.json
Continue only after cURL exits successfully and the saved response contains a public task ID. The documented submission response has a top-level task_id and starts with status: "queued"; that means submission, not a finished video.
Extract the identifier into a separate file, encoding it for the query URL:
# Save as read_task_id.py, then run: python3 read_task_id.py > task-id.txt
import json
from pathlib import Path
from urllib.parse import quote
response = json.loads(Path("submission.json").read_text(encoding="utf-8"))
task_id = response.get("task_id") if isinstance(response, dict) else None
if not isinstance(task_id, str) or not task_id.strip():
raise SystemExit("No public task_id: inspect submission.json before continuing.")
print(quote(task_id, safe=""))
The examples deliberately separate submission from checking and downloading. If the POST times out, its outcome is uncertain: investigate the saved response and available request records before sending it again.
Get the finished video
The task query reference uses the public ID from creation:
TASK_ID=$(cat task-id.txt)
curl --silent --show-error --fail-with-body --max-time 60 \
--header "Authorization: Bearer ${TOKENHOT_API_KEY:?Set TOKENHOT_API_KEY first}" \
--output status.json \
"https://api.tokenhot.ai/v1/video/generations/${TASK_ID}"
Run this only after the extraction command succeeds. The query's outer code: "success" means the lookup worked. Its documented completion example uses data.status: "SUCCESS" for the video task.
Save the following as read_video_url.py, then run python3 read_video_url.py > video-url.txt:
import json
from pathlib import Path
from urllib.parse import urlsplit
response = json.loads(Path("status.json").read_text(encoding="utf-8"))
if not isinstance(response, dict) or response.get("code") != "success":
raise SystemExit("Query did not succeed: inspect status.json.")
data = response.get("data")
if not isinstance(data, dict) or data.get("status") != "SUCCESS":
raise SystemExit("No confirmed completed task: inspect its status before continuing.")
url = data.get("result_url")
details = data.get("data")
upstream = details.get("output") if isinstance(details, dict) else None
if not url and isinstance(upstream, dict):
url = upstream.get("video_url")
if not isinstance(url, str) or not url.strip():
raise SystemExit("Completed task has no result URL: keep its response and task ID.")
parsed = urlsplit(url)
if parsed.scheme != "https" or not parsed.netloc:
raise SystemExit("Inspect the returned URL before downloading; this helper expects HTTPS.")
print(url)
If the job is still processing, repeat the GET later and inspect its status. A production client should use a polling interval and deadline appropriate to its workload, and distinguish processing from failed or canceled jobs. This helper stops whenever completion is unconfirmed; it does not invent a complete gateway-status list from the single success example.
The response can also contain an upstream ID at data.data.output.task_id. Keep querying with the original public task ID, not that nested identifier.
After the URL extraction succeeds, save the video in a separate, unauthenticated download:
VIDEO_URL=$(cat video-url.txt)
curl --silent --show-error --fail-with-body --location \
--proto '=https' --proto-redir '=https' --max-time 300 \
--output video.mp4 "$VIDEO_URL"
The documented data.result_url and nested data.data.output.video_url are signed temporary URLs. Download promptly and keep the URL file private. Open the saved video and check playback, the intended action, framing, and any important product details; an HTTP success or an MP4 filename does not establish visual quality.
Extend a clip instead of generating a new scene
Suppose you have a 10-second lamp shot and want the camera movement to continue for five more seconds. This is an authored request example, not a measured result.
Prepare a publicly accessible URL for the clip you are entitled to use. The reference-video limits are separate from the generation budget: the documentation allows MP4/MOV input of 1–15 seconds, at least 16 fps, with each side 240–4096 pixels, an aspect ratio no greater than 8:1, and a maximum file size of 100 MB. Confirm that the URL serves the actual video without a browser login.
In a fresh job folder, save this as request.json, replacing the example URL:
{
"model": "wan3.0-video",
"input": {
"prompt": "Continue the camera movement from video 1 for five more seconds toward the lamp. Keep the same lighting, desk, and subject; avoid a scene cut.",
"media": [
{
"type": "reference_video",
"url": "https://example.com/your-10-second-lamp-clip.mp4"
}
]
},
"parameters": {
"resolution": "720P",
"ratio": "adaptive",
"duration": 5,
"audio": false
}
}
Reuse the same POST, ID extraction, GET, and download commands in this new folder. Save the returned task ID independently from the earlier job.
Three choices matter here:
- Reference the clip, not an old task ID. This documented Wan request uses
reference_videowith a media URL. It does not use an invented/extendendpoint ortaskIdfield. - Keep
ratioset toadaptive. That is a documented requirement for extension, even if your original shot was landscape. - Budget input and output together. With video input, their combined duration must not exceed 30 seconds. The example fits because 10 + 5 = 15 seconds. A short requested output does not make a reference clip longer than the separate 15-second input limit acceptable.
The prompt's preservation instructions describe what you want. They do not prove seamless motion, unchanged pixels, or an output file containing the original clip untouched. Inspect what was returned before using it in an edit. Likewise, a URL from an earlier successful generation may have expired before the next request; provide a still-accessible reference rather than assuming that a saved task ID keeps the media URL usable.
Start from an image when the opening frame matters
If you have a product photo instead of a clip, keep the same request envelope and replace input with an image-and-motion instruction:
{
"prompt": "The camera slowly moves closer to the lamp in the opening frame. Keep the desk still and maintain the soft afternoon lighting.",
"media": [
{"type": "first_frame", "url": "https://example.com/your-lamp-first-frame.jpg"}
]
}
This block is the value of input, not a complete POST body. Retain the outer model and suitable parameters from the first example. Replace the placeholder image URL with an image you may use that the service can retrieve.
You can add one last_frame item when the intended ending matters. First/last-frame mode allows those frame types only: do not combine them with reference_image, reference_video, reference_audio, file, or link. A reference image serves a different role from a fixed opening frame; select the mode for the shot you need.
For these images, the documented formats include JPEG/JPG, PNG without an alpha channel, BMP, and WEBP. Each side must be 240–8000 pixels, the aspect ratio at most 8:1, and each file no larger than 20 MB. Without video input, an explicit output duration must be an integer from 2 to 30 seconds.
Recover at the stage that failed
| What happened | What to do next |
|---|---|
| Submission timed out or no public task ID was confirmed | Preserve the request and response; investigate whether a job was created before submitting again. |
| A task lookup failed | Keep the public ID and diagnose the GET; do not treat the lookup failure as a reason to regenerate. |
| The task is not confirmed successful | Inspect its actual status and any error details. Continue polling only for a processing job. |
| The task completed but the URL is missing | Keep the response and task ID, and investigate the result fields before downloading or resubmitting. |
| Download failed or the signed URL expired | Revisit the existing task and inspect its result. A fresh query does not guarantee renewal of an expired URL. |
| The saved clip plays but the movement or product details are wrong | Review the input and prompt before deliberately creating a new job; that is another generation, not download recovery. |
Start with a short shot whose action you can judge, save its result, and extend it only when the continuation serves your scene. That gives you a useful clip to evaluate before committing to a larger video workflow.


