30s Videos, 50+ References. Try It Now
← Back to blog

Free to Test Wan 3.0 API: A Beginner's Guide with Node.js

Alex12 min read
Free to Test Wan 3.0 API: A Beginner's Guide with Node.js
Contents

You can test the Wan 3.0 API on SeeGen AI with 200 free credits after signing up and joining our Discord. This tutorial uses 192 credits to turn one reference image into a six-second, 720p video.

The example is an anime dance sheet with sixteen poses of the same character. We'll upload that image, ask Wan 3.0 to animate one dancer, and get a video URL that you can open in your browser. The code runs in Node.js, and you can follow along in VS Code.

Keep the default settings for your first run. They fit within the free credit balance.

See the image-to-video example

The starting image shows a girl with a black bob, a teal jacket, and a coral skirt. Each panel gives her a different dance pose.

input
input

The prompt asks for a short dance in a single scene. The sheet guides her appearance and movement; the finished video should show one character without the panel borders or numbers.

Our YouTube walkthrough covers the upload, request, and task-status steps. It uses a ten-second example. The code in this article uses six seconds so you can try the workflow with the free credits.

Wan 3.0 Beginner Guide

Six seconds gives us less room for choreography. We ask for a few connected movements instead of trying to fit every pose into the clip.

How to get 200 free credits for Wan 3.0

1. Create an account on SeeGen AI.
2. Join our Discord and follow the instructions to claim the new-user credits.
3. Check that the 200 credits appear in your account before running the script.

The free plan includes API access. Creating an API key and claiming credits are separate steps, so check both before submitting a request.

What can you generate for free?

These examples use the standard `wan3.0-video` model with one reference image:

Video SettingsCreditsFits within Free Plan?
480p, 6 seconds96Yes
720p, 6 seconds192Yes
720p, 10 seconds320No
credits needed

At 720p, the rate is 32 credits per second: 6 x 32 = 192. Starting with 200 credits leaves 8 after this request, assuming you haven't used any elsewhere. Check the [current API pricing](https://seegen.ai/api-docs) before changing settings. The Prime model has different rates.

The free offer covers a test with credits. Further generations need enough remaining credit or a top-up.

Create your API key and prepare the project

Open your account page, find the API key section, and create a key. A name such as `Wan Test` makes it easy to recognize later.

The key is shown only once. Store it somewhere safe, keep it hidden during recordings, and leave it out of public code.

Install Node.js version 22 or newer. Open a folder called `seegen-api` in VS Code. This example uses Node's built-in tools, so you don't need to install extra packages.

Create the following files and add the dance sheet as `input.png`:

seegen-api/
.env
.gitignore
package.json
generate.mjs
input.png

Use a real PNG file. Changing a JPG's filename to end in `.png` does not convert its format. Click the image in VS Code to check that you have the right file.

In `.env`, add your key:

API_KEY=YOUR_SEEGEN_API_KEY

Replace the placeholder with your own value. The `.gitignore` file keeps it out of new Git commits:

.env
.env.*
!.env.example
node_modules/
last-task.json
result.json
.DS_Store

It won't remove a key that you already committed. If you accidentally publish a key, revoke it and create another.

Add this to `package.json`:

{
"name": "seegen-api",
"private": true,
"type": "module",
"engines": { "node": ">=22" },
"scripts": {
"start": "node --env-file=.env generate.mjs"
}
}

Upload the image and send your first request

The script uploads `input.png` before it requests a video. It sends the file as multipart form data to `/assets/upload?model=wan3.0-video`, with your API key in the Authorization header.

Use the returned HTTPS image URL in the generation request. You don't need to copy it yourself; the script passes it along. Let `FormData` set the upload's Content-Type header so it includes the correct boundary.

The settings used in this example

The request uses `videoInputMode: "reference"` because the image is a pose sheet. In keyframe mode, an image defines a frame of the video, which is a different starting point for this case.

The other settings are `duration: "6s"`, `outputResolution: "720p"`, and `ratio: "16:9"`. Audio is enabled, and `promptExtend: false` turns off automatic prompt expansion. See the Wan API reference for the supported inputs.

The prompt calls the image `Image1`. It asks the dancer to step sideways, raise her arms, make a small jump, and return to her starting stance. A fixed camera and plain background make the movement easier to judge.

Complete Node.js script

Put this code in `generate.mjs`:

import { readFile, writeFile } from "node:fs/promises";
import { setTimeout as sleep } from "node:timers/promises";
import { pathToFileURL } from "node:url";

const BASE_URL = "https://seegen.ai/api/v1";
const MODEL = "wan3.0-video";
const POLL_INTERVAL_MS = 5000;
const MAX_NETWORK_FAILURES = 5;
const WAIT_TIMEOUT_MS = 15 * 60 * 1000;
const IMAGE_FILE = new URL("./input.png", import.meta.url);
const TASK_FILE = new URL("./last-task.json", import.meta.url);
const RESULT_FILE = new URL("./result.json", import.meta.url);

const PROMPT = `Create a 6-second anime dance video using Image1 as the character and pose reference.
The 4x4 grid shows consecutive poses of ONE dancer, read left to right, top to bottom.
Animate one full-body character in a single scene. Use the sheet to guide a short dance;
do not rush through all sixteen poses.
0-2s: Step left and right with a gentle arm swing.
2-4s: Open both arms, then raise them into a V for a small jump and soft landing.
4-6s: Lower the arms and return to the opening stance.
Preserve the black bob with teal highlights, star hair clip, teal jacket, coral skirt, and sneakers.
Keep the face and body proportions consistent. Natural weight shifts, stable foot contact,
and gentle hair and clothing movement. Fixed camera, full body always visible,
clean white background, crisp 2D anime style. Upbeat instrumental pop music synchronized
with the dance, no vocals. No grid, panel borders, numbers, duplicate characters,
scene cuts, or extra limbs.`;

function apiKey() {
const key = process.env.API_KEY?.trim();
if (!key || key === "YOUR_SEEGEN_API_KEY") {
throw new Error("Add your API_KEY to .env, then run: npm start");
}
return key;
}

function errorText(value) {
return typeof value === "string" ? value : JSON.stringify(value);
}

class NetworkError extends Error {}

export async function apiRequest(path, { timeoutMs = 60000, ...options } = {}) {
let response;
let text;
try {
response = await fetch(`${BASE_URL}${path}`, {
...options,
headers: { ...options.headers, Authorization: `Bearer ${apiKey()}` },
signal: AbortSignal.timeout(timeoutMs),
redirect: "error",
});
text = await response.text();
} catch (cause) {
const detail = cause.cause?.code ?? cause.cause?.message;
throw new NetworkError(`Network error at ${path}: ${cause.message}${detail ? ` (${detail})` : ""}`);
}
let data;
try {
data = JSON.parse(text);
} catch {
throw new Error(`HTTP ${response.status}: server returned a non-JSON response.`);
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${errorText(data.message ?? data.error ?? data)}`);
}
return data;
}

export async function uploadImage() {
const bytes = await readFile(IMAGE_FILE);
const form = new FormData();
form.append("file", new Blob([bytes], { type: "image/png" }), "input.png");

console.log("Uploading input.png...");
let asset = await apiRequest(`/assets/upload?model=${MODEL}`, {
method: "POST",
body: form,
// FormData sets Content-Type, including its multipart boundary.
});
const uploadedUrl = asset.url;
const assetId = asset.assetId;
const deadline = Date.now() + 2 * 60 * 1000;
while (asset.status === "PROCESSING") {
if (assetId == null || Date.now() >= deadline) {
throw new Error("Image is not ready. Check its upload status before generating.");
}
await sleep(POLL_INTERVAL_MS);
asset = await apiRequest(`/assets/status?assetId=${encodeURIComponent(assetId)}`);
}
if (asset.status !== "ACTIVE") {
throw new Error(`Image upload is not active: ${errorText(asset.failReason ?? asset.status)}`);
}
const imageUrl = asset.url ?? uploadedUrl;
if (typeof imageUrl !== "string" || !imageUrl.startsWith("https://")) {
throw new Error("Upload did not return an HTTPS image URL.");
}
console.log("Image uploaded successfully.");
return imageUrl;
}

export async function createTask(imageUrl) {
const taskRequest = {
model: MODEL,
inputs: {
urls: [imageUrl],
videoInputMode: "reference",
prompt: PROMPT,
duration: "6s",
outputResolution: "720p",
ratio: "16:9",
generateAudio: true,
promptExtend: false,
},
};

console.log("Starting a new Wan 3.0 generation: 6s, 720p, 16:9.");
console.log("This request uses account credits.");
// Submit once. A lost response can still mean the server accepted the task.
let task;
try {
task = await apiRequest("/jobs/createTask", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(taskRequest),
});
} catch (error) {
console.error("Submission was not retried. If its outcome is unclear, check your task history before starting again.");
throw error;
}
if (typeof task.taskId !== "string" || !task.taskId) {
throw new Error("No taskId returned. Check task history before submitting again.");
}
return task.taskId;
}

export async function waitForResult(taskId, {
pollIntervalMs = POLL_INTERVAL_MS,
timeoutMs = WAIT_TIMEOUT_MS,
} = {}) {
const deadline = Date.now() + timeoutMs;
let networkFailures = 0;
while (Date.now() < deadline) {
let result;
try {
result = await apiRequest(`/jobs/queryTask?taskId=${encodeURIComponent(taskId)}`, {
timeoutMs: Math.min(30000, Math.max(1, deadline - Date.now())),
});
networkFailures = 0;
} catch (error) {
if (!(error instanceof NetworkError)) throw error;
networkFailures += 1;
console.warn(`Status check interrupted (${networkFailures}/${MAX_NETWORK_FAILURES}): ${error.message}`);
if (networkFailures >= MAX_NETWORK_FAILURES) throw error;
await sleep(Math.min(pollIntervalMs, Math.max(0, deadline - Date.now())));
continue;
}
console.log(`Status: ${result.status}`);
if (result.status === "COMPLETED") {
const videoUrl = result.output?.[0]?.url;
if (typeof videoUrl !== "string" || !videoUrl.startsWith("https://")) {
throw new Error("Task completed without an HTTPS video URL. Query this task again later.");
}
return result;
}
if (result.status === "FAILED") {
throw new Error(`Generation failed: ${errorText(result.error ?? "No error details")}`);
}
if (!["PENDING", "PROCESSING"].includes(result.status)) {
throw new Error(`Unexpected task status: ${result.status}. Keep the task ID for follow-up.`);
}
await sleep(Math.min(pollIntervalMs, Math.max(0, deadline - Date.now())));
}
throw new Error("Stopped waiting after the local timeout. The server task may still be running.");
}

export async function main() {
apiKey();
// Passing an existing task ID skips upload and generation entirely.
let taskId = process.argv[2];
if (!taskId) {
taskId = await createTask(await uploadImage());
console.log(`Task ID: ${taskId}`);
try {
await writeFile(TASK_FILE, JSON.stringify({ taskId }, null, 2) + "\n");
} catch {
console.warn("Could not save last-task.json. Keep the task ID shown above.");
}
} else {
console.log(`Checking existing task: ${taskId}`);
}
console.log(`To check this task later: npm start -- ${taskId}`);
const result = await waitForResult(taskId);
console.log(`Credits used: ${result.creditsUsed ?? "unknown"}`);
console.log(`Final video URL: ${result.output[0].url}`);
await writeFile(RESULT_FILE, JSON.stringify(result, null, 2) + "\n");
console.log("Response saved to result.json. Open the video URL to view or download.");
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
}

Run the script and open your video

Open the VS Code terminal in your project folder and run:

npm start

The script uploads the image, creates a task, and prints its ID. It also saves that ID in `last-task.json`. Keep it: you can use it to check the same generation later.

Video generation takes time. The initial response gives you a task ID while the server works on the video. This script waits five seconds between status checks. Each check can take up to thirty seconds, and five consecutive network failures stop the local polling. A successful check resets that failure count.

The script also has a fifteen-minute waiting limit. Reaching it does not cancel the server task.

When the task completes, the terminal shows the credits used and the final video URL. Open that URL to view or download the result. The script saves the response in `result.json` too.

Watch the feet when she lands. Check whether her face and clothes stay consistent, and whether the grid disappears. Those details give you a more useful first check than simply deciding whether the clip looks good.

wan 3.0 image to video

FAQs and troubleshooting

Why does the upload stop with "terminated"?

During setup for this tutorial, the terminal showed `Uploading input.png...` followed by `terminated`. That message alone did not identify the cause.

Check that the image opens and your connection is working. This script reports the failing endpoint and any available network error details. If the error occurs during upload, it has not yet reached the generation step. If it occurs while submitting a video request, check task history before trying again.

How do I continue checking a task?

Copy the full task ID from the terminal or `last-task.json`, then run:

npm start -- YOUR_TASK_ID

Replace `YOUR_TASK_ID` with the actual ID. This command only checks the existing task. Running `npm start` without an ID uploads the image and submits a new generation.

The script retries interrupted status checks, but it does not automatically repeat a creation request. A lost response can still mean the server accepted the task.

Why do I get an insufficient-credits error?

Check your remaining balance and the settings in the request. This example needs 192 credits. A ten-second, 720p request needs 320, so the 200-credit gift does not cover it. Credits spent on other tasks also reduce what is available for this test.

Can I use my own image?

Yes. Replace `input.png` with your PNG and rewrite the prompt to match it. Remove the dance-sheet instructions if your image shows something else. Start with a small amount of movement that you can describe clearly.

Will Wan 3.0 follow all sixteen poses exactly?

The sheet guides the request, but exact pose matching is not guaranteed. This six-second prompt deliberately uses only a few movements. Review the result before deciding whether to change the prompt or spend more credits on another version.

Can I run this without VS Code?

Yes. VS Code is the editor used in this guide. You can run the same command from another terminal with Node.js installed, using the project folder as your working directory.

Start your free Wan 3.0 API test

Create your SeeGen AI account, join Discord, and claim the 200 credits. Use the six-second, 720p settings for your first request, then save the task ID so you can return to the result without submitting again.

Related articles