alibaba/wan-3.0-prime/reference-to-videoFrom 46 credits for a 5-second clip· Reference to VideoWan 3.0 Prime reference-to-video holds a face, wardrobe, location or voice across a clip using up to 10 images, 5 videos and 5 audio files. It returns 2 to 30 seconds at 480p to 1080p, with reference video and audio each capped at 15 seconds.
Call it with the model name alibaba/wan-3.0-prime/reference-to-video on POST /jobs/createTask, from 46 credits for a 5-second clip ($0.46). New accounts get 30 free credits to try it in the Playground first.
- Create Video
- Image to Video
- Reference to Video
- Extend Video
- Up to 1080p
- 2–30s clips
- Sound included
- Start and end frames
- Up to 10 reference images
- 5 aspect ratios
alibaba/wan-3.0-prime/reference-to-videoPOST https://api.popnow.ai/api/v1/jobs/createTask# 1. Create the task. Keep the Idempotency-Key if you retry after a network error.
export IDEMPOTENCY_KEY="$(uuidgen)"
curl -X POST https://api.popnow.ai/api/v1/jobs/createTask \
-H "Authorization: Bearer $POPNOW_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
-d '{
"model": "alibaba/wan-3.0-prime/reference-to-video",
"input": {
"prompt": "A slow aerial shot drifting over a misty pine forest at sunrise",
"reference_image_urls": [
"https://media.popnow.ai/influencer/examples/ava.webp"
],
"aspect_ratio": "16:9",
"resolution": "720p",
"duration_seconds": 5
}
}'
# 2. Copy data.taskId from the response, then poll until state is succeeded, failed, canceled or expired.
TASK_ID="TASK_ID_FROM_RESPONSE"
curl "https://api.popnow.ai/api/v1/jobs/recordInfo?taskId=$TASK_ID" \
-H "Authorization: Bearer $POPNOW_API_KEY"// Node 18+ (or any runtime with fetch). Keep the API key on your server.
const BASE = "https://api.popnow.ai/api/v1";
const headers = { Authorization: `Bearer ${process.env.POPNOW_API_KEY}` };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function call(path, init = {}) {
const res = await fetch(BASE + path, { ...init, headers: { ...headers, ...init.headers } });
const body = await res.json();
if (body.code !== 200) throw new Error(`${body.code} ${body.msg}`);
return body.data;
}
const { taskId } = await call("/jobs/createTask", {
method: "POST",
headers: { "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({
"model": "alibaba/wan-3.0-prime/reference-to-video",
"input": {
"prompt": "A slow aerial shot drifting over a misty pine forest at sunrise",
"reference_image_urls": [
"https://media.popnow.ai/influencer/examples/ava.webp"
],
"aspect_ratio": "16:9",
"resolution": "720p",
"duration_seconds": 5
}
}),
});
// Poll with backoff. A finished task may still be copying files: wait while an asset is pending.
let task;
for (let delay = 2000; ; delay = Math.min(delay * 1.5, 15000)) {
await sleep(delay);
task = await call(`/jobs/recordInfo?taskId=${taskId}`);
const copying = (task.output?.assets ?? []).some((a) => a.pending && !a.unavailable);
if (["succeeded", "failed", "canceled", "expired"].includes(task.state) && !copying) break;
}
if (task.state !== "succeeded") throw new Error(`${task.state}: ${task.errorMessage ?? "no detail"}`);
// Each asset has a signed url (no API key needed) that expires at expiresAt: save the file.
for (const asset of task.output.assets) console.log(asset.url);import os, time, uuid, requests
BASE = "https://api.popnow.ai/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['POPNOW_API_KEY']}"}
def call(method, path, **kwargs):
body = requests.request(method, BASE + path, headers={**HEADERS, **kwargs.pop("headers", {})}, timeout=30, **kwargs).json()
if body["code"] != 200:
raise RuntimeError(f'{body["code"]} {body["msg"]}')
return body["data"]
task_id = call(
"POST",
"/jobs/createTask",
headers={"Idempotency-Key": str(uuid.uuid4())}, # reuse it if you retry
json={
"model": "alibaba/wan-3.0-prime/reference-to-video",
"input": {
"prompt": "A slow aerial shot drifting over a misty pine forest at sunrise",
"reference_image_urls": [
"https://media.popnow.ai/influencer/examples/ava.webp"
],
"aspect_ratio": "16:9",
"resolution": "720p",
"duration_seconds": 5
}
},
)["taskId"]
# Poll with backoff. A finished task may still be copying files: wait while an asset is pending.
delay = 2.0
while True:
time.sleep(delay)
task = call("GET", "/jobs/recordInfo", params={"taskId": task_id})
assets = (task.get("output") or {}).get("assets") or []
copying = any(a.get("pending") and not a.get("unavailable") for a in assets)
if task["state"] in ("succeeded", "failed", "canceled", "expired") and not copying:
break
delay = min(delay * 1.5, 15.0)
if task["state"] != "succeeded":
raise RuntimeError(f'{task["state"]}: {task.get("errorMessage", "no detail")}')
# Each asset has a signed url (no API key needed) that expires at expiresAt: save the file.
for asset in assets:
print(asset["url"])The input is built from the schema below, so it passes validation as written. Change the prompt and links to your own.
Price
| Option | Credits per second | ≈ USD |
|---|---|---|
resolution 480p | 9.18 | $0.092 |
resolution 720p | 18.9 | $0.19 |
resolution 1080p | 37.8 | $0.38 |
1 credit = $0.01 · Every option and its price: GET /models/alibaba/wan-3.0-prime/reference-to-video
Each task is rounded up to whole credits, so one run at the lowest tier costs 46 credits for a 5-second clip ($0.46).
Limits
- Clip length
- 2–30 s
- Resolutions
- 480p · 720p · 1080p
- Aspect ratios
- 16:9 · 9:16 · 1:1 · 4:3 · 3:4 · adaptive
- Reference audio
- 5
- Reference images
- 10
- Reference videos
- 5
- Longest input clip
- 15 s
Parameters
These fields come from the same schema we check every request against. Fields that are not listed are rejected.
Some fields depend on each other (for example, at least one reference). The raw schema at the bottom has the exact rules.
promptstringOptionalDescribe the subject, action, camera, light, pacing, and boundaries for the shot.
reference_image_urlsarray<string>Sometimes requiredPictures of the people, outfits or places the result should keep.
reference_video_urlsarray<string>Sometimes requiredChanges the priceClips whose motion, camera or style the result should follow.
reference_audio_urlsarray<string>Sometimes requiredAudio to match: a voice, music or a rhythm.
reference_file_urlstringSometimes requiredOne reference document (PDF, DOCX, TXT, MD and similar) that the model reads for characters, wardrobe, setting and action.
reference_link_urlstringSometimes requiredOne public web page the model reads for reference. It must open without a login.
aspect_ratiostringOptionalThe shape of the frame: wide, square or tall.
enable_thinkingbooleanOptionalLet the model reason about the brief and the reference material before generating. Helps with complex references; slower. Switches on by itself when a reference file or link is given.
resolutionstringOptionalChanges the priceOutput size. Bigger sizes look sharper and cost more.
duration_secondsintegerOptionalChanges the priceHow long the clip runs. You pay per second.
generate_audiobooleanOptionalAdds sound made to match the picture: voices, ambience and effects.
enable_prompt_expansionbooleanOptionalLets the model add detail to a short prompt.
seedintegerOptionalReuse the same number to get a repeatable result.
Raw JSON Schema
{
"additionalProperties": false,
"anyOf": [
{
"required": [
"reference_image_urls"
]
},
{
"required": [
"reference_video_urls"
]
},
{
"required": [
"reference_audio_urls"
]
},
{
"required": [
"reference_file_url"
]
},
{
"required": [
"reference_link_url"
]
}
],
"properties": {
"aspect_ratio": {
"default": "16:9",
"description": "Output aspect ratio; adaptive follows the input media and the intent.",
"enum": [
"16:9",
"9:16",
"1:1",
"4:3",
"3:4",
"adaptive"
],
"type": "string"
},
"duration_seconds": {
"default": 5,
"description": "Requested output length in seconds. Pass -1 for smart duration: the model picks the length from the prompt and any input media instead of a fixed number. With reference videos, their total length plus the output length must not exceed 30 seconds, and the reference video seconds are billed together with the output seconds.",
"enum": [
-1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30
],
"type": "integer"
},
"enable_prompt_expansion": {
"description": "Allow the service to expand the submitted creative brief before generation.",
"type": "boolean"
},
"enable_thinking": {
"default": false,
"description": "Let the model reason about the brief and the reference material before generating. Helps with complex references; slower. Switches on by itself when a reference file or link is given.",
"type": "boolean"
},
"generate_audio": {
"default": true,
"description": "Request generated audio with the video.",
"type": "boolean"
},
"prompt": {
"description": "Describe the subject, action, camera, light, pacing, and boundaries for the shot.",
"minLength": 1,
"type": "string"
},
"reference_audio_urls": {
"description": "Reference audio, up to 5 items and 15 seconds in total.",
"items": {
"contentMediaType": [
"audio/mpeg",
"audio/wav"
],
"type": "string"
},
"maxItems": 5,
"minItems": 1,
"type": "array"
},
"reference_file_url": {
"contentMediaType": [
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.apple.keynote",
"application/vnd.apple.pages",
"application/vnd.apple.numbers",
"text/plain",
"text/markdown"
],
"description": "One reference document (PDF, DOCX, TXT, MD and similar) that the model reads for characters, wardrobe, setting and action.",
"format": "uri",
"type": "string"
},
"reference_image_urls": {
"description": "Reference images, up to 10 items.",
"items": {
"contentMediaType": [
"image/jpeg",
"image/png",
"image/webp",
"image/gif"
],
"type": "string"
},
"maxItems": 10,
"minItems": 1,
"type": "array"
},
"reference_link_url": {
"description": "One public web page the model reads for reference. It must open without a login.",
"format": "uri",
"type": "string"
},
"reference_video_urls": {
"description": "Reference videos, up to 5 items and 15 seconds in total.",
"items": {
"contentMediaType": [
"video/mp4",
"video/webm",
"video/quicktime"
],
"type": "string"
},
"maxItems": 5,
"minItems": 1,
"type": "array"
},
"resolution": {
"default": "720p",
"description": "Output resolution.",
"enum": [
"480p",
"720p",
"1080p"
],
"type": "string"
},
"seed": {
"description": "Random seed. Keep it fixed only for controlled comparisons, not as a frame-identical guarantee.",
"maximum": 2147483647,
"minimum": 0,
"type": "integer"
}
},
"type": "object"
}A real result from Wan 3.0 Prime
Take the camera behaviour from Video 1 — a slow handheld orbit — and apply it to the woman in Image 1, standing in an empty studio. Keep her face and outfit from Image 1. Soft top light, quiet room.
{
"model": "alibaba/wan-3.0-prime/reference-to-video",
"input": {
"prompt": "Take the camera behaviour from Video 1 — a slow handheld orbit — and apply it to the woman in Image 1, standing in an empty studio. Keep her face and outfit from Image 1. Soft top light, quiet room.",
"reference_image_urls": [
"https://media.popnow.ai/influencer/examples/ava.webp"
],
"aspect_ratio": "16:9",
"resolution": "720p",
"duration_seconds": 5
}
}This one started from an uploaded file: put your own link where the example link is.
Responses, polling and callbacks
Every model works the same way: create a task, then poll it or wait for a callback.
Price it first
curl -X POST https://api.popnow.ai/api/v1/jobs/quote \
-H "Authorization: Bearer $POPNOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "alibaba/wan-3.0-prime/reference-to-video",
"input": {
"prompt": "A slow aerial shot drifting over a misty pine forest at sunrise",
"reference_image_urls": [
"https://media.popnow.ai/influencer/examples/ava.webp"
],
"aspect_ratio": "16:9",
"resolution": "720p",
"duration_seconds": 5
}
}'Same body as createTask. It returns the expected and the highest possible charge, and runs nothing.
Accepted
HTTP/1.1 202 Accepted
{
"code": 200,
"msg": "success",
"data": {
"taskId": "task_01k3m8x9q2z4v7n5p6r8s0t1w3",
"state": "queued"
},
"request_id": "req_example_create"
}HTTP 202 means the task was accepted and the credits are held, not charged yet. The final charge never goes above the hold.
Check status
curl "https://api.popnow.ai/api/v1/jobs/recordInfo?taskId=$TASK_ID" \
-H "Authorization: Bearer $POPNOW_API_KEY"Poll every few seconds with backoff and stop once state is succeeded, failed, canceled or expired.
Finished task
{
"code": 200,
"msg": "success",
"data": {
"taskId": "task_01k3m8x9q2z4v7n5p6r8s0t1w3",
"model": "alibaba/wan-3.0-prime/reference-to-video",
"state": "succeeded",
"output": {
"assets": [
{
"url": "https://cdn.popnow.ai/tasks/task_01k3m8x9q2z4v7n5p6r8s0t1w3/output.mp4?signature=FROM_RESPONSE",
"expiresAt": "2026-10-17T12:20:00Z",
"mime": "video/mp4",
"width": 1280,
"height": 720,
"durationSeconds": 5
}
]
},
"settled": true
},
"request_id": "req_example_record"
}Download from url before expiresAt. Need a fresh link later? Ask POST /common/download-url.
Verify a callback
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const app = express();
// express.raw keeps the exact bytes that were signed; express.json would not match.
app.post("/hooks/popnow", express.raw({ type: "application/json" }), (req, res) => {
const timestamp = req.header("X-Webhook-Timestamp");
const signature = req.header("X-Webhook-Signature") ?? "";
const body = JSON.parse(req.body.toString("utf8"));
const taskId = body.data?.taskId ?? body.task_id;
const digest = createHash("sha256").update(req.body).digest("hex");
const expected = createHmac("sha256", process.env.POPNOW_WEBHOOK_SECRET)
.update(`${taskId}.${timestamp}.${digest}`)
.digest("base64");
const ok = expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
// Check the signature first, then reject anything older than five minutes.
if (!ok || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.status(401).end();
res.status(200).end(); // answer fast, then fetch the task and store its files
});With callBackUrl set, we post the finished task to you. Check the signature against the raw body before you trust it.
Send a local file
curl -X POST https://api.popnow.ai/api/v1/common/upload-url \
-H "Authorization: Bearer $POPNOW_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contentType": "image/png", "bytes": 482113}'
# PUT the file bytes to data.uploadUrl with every header in data.headers, then commit it:
curl -X POST https://api.popnow.ai/api/v1/files/$FILE_ID/commit \
-H "Authorization: Bearer $POPNOW_API_KEY"
# The commit returns a popnow://f/... reference. Put it in any image, video or
# audio field of the task input instead of a public https URL.Public https links work as they are. Local files go through upload-url and commit first.

