Video Generation API Integration Guide
Integrate NeoSpark video generation with an API key: full workflow, pricing, error handling and best practices for Seedance 2.0/2.5, Kling 3.0, Wan 3.0 and more.
Last updated: August 20, 2026
API Key Integration Guide - Video Generation
This document explains how to integrate NeoSpark video generation into your application using an API key.
Version note: This document covers
seedance-2.0/seedance-2.0-fast/seedance-2.5/gemini-omni-flash-preview/kling-3.0/kling-3.0-omni/minimax-h3/wan3.0-video.
Table of Contents
- Quick Start
- Get an API Key
- Authentication
- Complete Video Generation Workflow
- Supported Models and Pricing
- Video Generation API Reference
- Assets API
- Video Watermark Removal API
- Error Handling
- Best Practices
- Reference Links
Quick Start
Basic Info
API Base URL: https://api.useneospark.com/api/v1
Authentication: X-API-Key: np_your_api_key_here
Minimal Working Example
Python:
import requests
import time
API_KEY = "np_your_api_key_here"
BASE_URL = "https://api.useneospark.com/api/v1"
# 1. Create a video generation task
response = requests.post(
f"{BASE_URL}/video/generations",
headers={
"X-API-Key": API_KEY,
"Content-Type": "application/json"
},
json={
"prompt": "A cute cat playing on the grass",
"model": "seedance-2.0",
"duration": 5,
"ratio": "16:9",
"resolution": "720p"
}
)
result = response.json()
print(f"Task ID: {result['data']['task_id']}")
print(f"Estimated cost: {result['data']['pricing']['estimated_cost']} credits")
# 2. Poll for task status
task_id = result["data"]["task_id"]
while True:
response = requests.get(
f"{BASE_URL}/video/generations/{task_id}",
headers={"X-API-Key": API_KEY}
)
status_data = response.json()["data"]
print(f"Status: {status_data['status']}, Progress: {status_data['progress']}%")
if status_data["status"] == "completed":
print(f"Video URL: {status_data['video_url']}")
break
elif status_data["status"] in ["failed", "cancelled"]:
print(f"Task failed: {status_data.get('error_msg', 'Unknown error')}")
break
time.sleep(5)
JavaScript:
const API_KEY = "np_your_api_key_here";
const BASE_URL = "https://api.useneospark.com/api/v1";
async function generateVideo() {
// 1. Create a video generation task
const createResponse = await fetch(`${BASE_URL}/video/generations`, {
method: "POST",
headers: {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
prompt: "A cute cat playing on the grass",
model: "seedance-2.0",
duration: 5,
ratio: "16:9",
resolution: "720p",
}),
});
const result = await createResponse.json();
console.log("Task ID:", result.data.task_id);
// 2. Poll for task status
const taskId = result.data.task_id;
while (true) {
const statusResponse = await fetch(`${BASE_URL}/video/generations/${taskId}`, {
headers: { "X-API-Key": API_KEY },
});
const statusData = (await statusResponse.json()).data;
console.log(`Status: ${statusData.status}, Progress: ${statusData.progress}%`);
if (statusData.status === "completed") {
console.log("Video URL:", statusData.video_url);
return statusData.video_url;
} else if (["failed", "cancelled"].includes(statusData.status)) {
throw new Error(`Task failed: ${statusData.error_msg || 'Unknown error'}`);
}
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
generateVideo();
Get an API Key
Method 1: Create via the Web Console
- Visit https://platform.useneospark.com/user
- Sign in with your Google account
- Go to “User” → “API Keys”
- Click “Create API Key”
- Copy the generated key (it is shown only once!)
Method 2: Create via API (requires an existing Bearer Token)
# 1. Log in first to get a Bearer Token (via browser OAuth)
# 2. Use the Bearer Token to create an API Key
curl -X POST "https://api.useneospark.com/api/v1/api-keys" \
-H "Authorization: Bearer YOUR_BEARER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "My App",
"expires_days": 90
}'
Response example:
{
"code": 200,
"data": {
"id": 1,
"name": "My App",
"api_key": "np_xxxxxxxxxxxxxxxxxxxx",
"is_active": true,
"expires_at": "2025-07-08T10:30:00",
"created_at": "2025-04-09T10:30:00"
}
}
Authentication
Two ways to pass your API key are supported:
Method 1: X-API-Key Header (Recommended)
curl "https://api.useneospark.com/api/v1/video/generations" \
-H "X-API-Key: np_your_api_key_here"
Method 2: Authorization Header
curl "https://api.useneospark.com/api/v1/video/generations" \
-H "Authorization: ApiKey np_your_api_key_here"
Complete Video Generation Workflow
Video generation uses an asynchronous task model. The workflow is:
1. Submit a task → returns task_id
2. Poll for task status
3. Get the video URL once the task completes
4. Download or use the video
Flow Diagram
┌─────────────┐ POST /video/generations ┌─────────────┐
│ Your App │ ───────────────────────────▶ │ NeoSpark │
│ │ │ Server │
│ │ ◀─────────────────────────── │ │
└─────────────┘ {task_id, status:pending} └─────────────┘
│
│ GET /video/generations/{task_id}
│ (poll every 2-5 seconds)
▼
┌─────────────┐
│ Completed? │── No ──▶ Keep polling
└─────────────┘
│ Yes
▼
┌─────────────┐
│ Get video │
│ URL │
└─────────────┘
Supported Models and Pricing
Model List
Video generation currently supports the following models:
| Model | Description | Supported Resolutions | Max Duration | Status |
|---|---|---|---|---|
seedance-2.0 | High quality, balanced quality and speed, supports 4K | 480p, 720p, 1080p, 4k | 15s | Available |
seedance-2.0-fast | Fast generation, higher speed, up to 720p | 480p, 720p | 15s | Available |
seedance-2.5 | Even higher quality, supports 4K | 480p, 720p, 1080p, 4k | 30s | Available |
gemini-omni-flash-preview | Google Vertex AI short video generation with native audio output | 720p | 15s | Available |
kling-3.0 | Tencent Cloud VOD Kling 3.0 | 720p, 1080p, 2k, 4k | 15s | Available |
kling-3.0-omni | Tencent Cloud VOD Kling 3.0 Omni multimodal edition | 720p, 1080p, 2k, 4k | 15s | Available |
minimax-h3 | Next-generation open general-purpose multimodal video model; supports text-to-video, image-to-video, first/last frame, and multimodal references | 768p, 2k | 15s | Available |
wan3.0-video | Alibaba Cloud DashScope Wan 3.0; supports text-to-video, image-to-video, video-to-video, and reference audio | 480p, 720p, 1080p | 30s | Available |
Naming note: Internally, the code recognizes
doubao-seedance-2-0-260128/doubao-seedance-2-0-fast-260128/doubao-seedance-2.5as aliases ofseedance-2.0/seedance-2.0-fast/seedance-2.5, but the public API always usesseedance-2.0/seedance-2.0-fast/seedance-2.5.MiniMax-H3 note: The public API uses
minimax-h3; the actual model ID on the Moyu gateway isMiniMax-H3(case-sensitive).
Credit Calculation
The pricing.estimated_cost returned by the API is the final estimated charge, and frontends should rely on that value. The internal calculation logic is documented below for reference:
For retail credit packages, see the NeoSpark pricing page — e.g. $18 for 2,000 credits.
Seedance 2.0 family: Estimated using the new Volcano Engine token pricing:
tokens = duration × width × height × 24(fps) // 1024
estimated cost = ceil(tokens × price_per_million / 70_000 × multiplier)
price_per_million varies with resolution and whether an input video is included (roughly $2.20–7.10 per million tokens), and multiplier is currently fixed at 2.0. The actual charge never exceeds the frozen estimate.
Gemini Omni Flash:
estimated cost = ceil(15 × duration / 0.79)
Tencent Cloud Kling models: Converted from Kling’s official USD pricing (USD 1 ≈ 274 credits):
kling-3.0720p baseline: about $0.084/second (no audio) / $0.126/second (with audio)kling-3.0-omniadds roughly a 20% premium on top ofkling-3.0- Resolution factors:
1080p=4/3,2k≈2.5,4k=$0.420/second,480p=0.8
MiniMax-H3 (keeps the 0.79 discount compensation):
estimated cost = ceil(duration × 0.8 / 0.07 / 0.79)
- Moyu pricing is $0.11/second, converted to credits with the 0.79 discount compensation applied.
- Example: about 73 credits for 5 seconds, about 145 credits for 10 seconds.
Alibaba Cloud Wan 3.0 (official list pricing, no 0.79 discount compensation): Converted from DashScope’s official pricing (shown in USD):
480p: $0.04/second → about 14 credits/second720p: $0.08/second → about 27 credits/second1080p: $0.17/second → about 55 credits/second
The backend first compresses images to JPEG and converts them to base64 data URIs before submission, to avoid DashScope cross-region download failures.
General notes:
0.79is the credit-purchase discount compensation factor, applied to some fixed-unit-price models (Omni, MiniMax-H3, etc.).- When a task is created, credits are frozen based on
estimated_cost. - After the task completes successfully, billing is settled at actual cost but never exceeds the frozen estimate; on failure the frozen credits are released.
- The
completion_tokens/total_tokens/token_costfields returned by query endpoints are for reference only.
Provider-Specific Limits
- Gemini Omni Flash:
- Only supports
720p - Maximum 15 seconds
- Tasks usually complete synchronously; the response may return
completedandvideo_urldirectly
- Only supports
- Tencent Cloud Kling:
- Does not support
480p - Upstream statuses:
PENDING/PROCESSING/FINISH/FAIL
- Does not support
- Seedance:
- Total reference images (including first/last frames): up to 9
- Up to 3 reference videos
- Asset images must be at least 300×300 pixels
- Assets uploaded to the Moyu asset library must reach
Activestatus before use (wait up to 120 seconds)
- MiniMax-H3:
- Only supports
768pand2kresolutions - Maximum 15 seconds
- Supports the
adaptiveaspect ratio (auto-fit) - Up to 9 reference images, 3 reference videos, and 3 reference audios
- Does not support
face_swap_modeorreal_person_mode - Media references are converted via the Moyu asset library into
mm_file://{file_id}before submission - Upstream statuses:
queued/in_progress/completed/failed
- Only supports
- Wan 3.0:
- Only supports
480p,720p,1080p - Maximum 30 seconds
- Supports the
adaptiveaspect ratio - Supports text-to-video, image-to-video (first/last frame), video-to-video, and reference audio
- Up to 9 reference images and 3 reference videos
- Does not support
face_swap_mode,real_person_mode,generate_audio, orwatermark - The backend compresses image assets to JPEG and converts them to base64 data URIs before submission
- Upstream statuses:
PENDING/RUNNING/SUCCEEDED/FAILED/CANCELLED
- Only supports
Reserved Fields with No or Limited Effect
The following fields are accepted but do not take effect; they are kept only for compatibility (except on Wan 3.0):
seedcamera_fixedreturn_last_framedraftframesfpsservice_tiertoolsreference_audio_*(reference audio is only supported by Wan 3.0)
Video Generation API Reference
1. Submit a Video Generation Task
POST /api/v1/video/generations
Creates a new video generation task.
Request parameters:
| Field | Type | Required | Description |
|---|---|---|---|
| prompt | string | Yes | Video description text, 1-2000 characters |
| model | string | No | Model name, default seedance-2.0. Options: seedance-2.0-fast, seedance-2.5, gemini-omni-flash-preview, kling-3.0, kling-3.0-omni, minimax-h3, wan3.0-video |
| duration | int | No | Video duration in seconds. Default 5. Seedance 2.0/2.0-fast/Kling/Omni/MiniMax-H3 support 4 |
| ratio | string | No | Aspect ratio, default 16:9. MiniMax-H3 additionally supports adaptive |
| resolution | string | No | Resolution, default 720p. Supported ranges differ per model; see “Supported Models and Pricing” |
| generate_audio | bool | No | Whether to generate audio, default false |
| watermark | bool | No | Whether to add a watermark, default false |
| real_person_mode | bool | No | Whether to enable real-person mode, default false (requires uploading assets via /video/assets first and waiting for Active status) |
| face_swap_mode | bool | No | Whether to enable precision face-swap mode (Seedance family only), default false. When enabled, provide 1 reference image + 1 reference video |
| negative_prompt | string | No | Negative prompt. In face_swap_mode it is passed through to the Seedance metadata; in normal mode its effect depends on upstream support |
| seed | int | No | Random seed (not supported by the current gateway; kept for compatibility) |
| camera_fixed | bool | No | Whether to fix the camera position (not supported by the current gateway; kept for compatibility) |
| return_last_frame | bool | No | Whether to return the last frame image (not supported by the current gateway; kept for compatibility) |
| draft | bool | No | Draft mode (not supported by the current gateway; kept for compatibility) |
| frames | int | No | Total number of video frames (not supported by the current gateway; kept for compatibility) |
| fps | int | No | Frame rate (not supported by the current gateway; kept for compatibility) |
| service_tier | string | No | Service tier (not supported by the current gateway; kept for compatibility) |
| tools | array | No | Tool calls (not supported by the current gateway; kept for compatibility) |
Resource reference parameters (image-to-video / multimodal reference):
Each resource can be provided in four ways (URL / upload_id / path / asset_id). They are mutually exclusive — choose one. To use
asset_id, first register the asset viaPOST /video/assetsorPOST /video/assets/from-upload, and wait until the asset status becomesActivebefore referencing it.
| Field | Type | Description |
|---|---|---|
| first_frame_url / first_frame_upload_id / first_frame_path / first_frame_asset_id | string | First frame image |
| last_frame_url / last_frame_upload_id / last_frame_path / last_frame_asset_id | string | Last frame image |
| reference_image_urls / reference_image_upload_ids / reference_image_paths / reference_image_asset_ids | array | Reference image list. Up to 9 for Seedance, up to 5 for Omni |
| reference_video_urls / reference_video_upload_ids / reference_video_paths | array | Reference video list, up to 3 |
| reference_audio_urls / reference_audio_upload_ids / reference_audio_paths | array | Reference audio list (only supported by Wan 3.0) |
Mixing rules:
reference_imagecannot be used together withfirst_frame/last_frame(normal mode, Seedance family).gemini-omni-flash-preview,minimax-h3, andwan3.0-videoallow reference images together with first/last frames. Real-person mode (real_person_mode=true) and precision face-swap mode (face_swap_mode=true) also allow them together.
Supported models:
seedance-2.0- High quality, supports 4Kseedance-2.0-fast- Fast generation, up to 720pseedance-2.5- Even higher quality, supports 4K and up to 30 secondsgemini-omni-flash-preview- Google Vertex AI; supports text-to-video / image-to-video and native audio; 720p onlykling-3.0- Tencent Cloud VOD Kling 3.0kling-3.0-omni- Kling 3.0 Omni multimodal editionminimax-h3- Next-generation open general-purpose multimodal video model; supports 768p/2k, text-to-video / image-to-video / first-last frame / multimodal referenceswan3.0-video- Alibaba Cloud DashScope Wan 3.0; supports text-to-video / image-to-video / video-to-video and reference audio
For actual billing logic see “Supported Models and Pricing”; frontends should rely on the pricing.estimated_cost returned by the API.
Supported aspect ratios:
16:9- Landscape (default)9:16- Portrait1:1- Square4:3,3:4,21:9,adaptive
Supported resolutions:
| Model | Supported Resolutions |
|---|---|
seedance-2.0 | 480p, 720p, 1080p, 4k |
seedance-2.0-fast | 480p, 720p |
gemini-omni-flash-preview | 720p |
kling-3.0 | 720p, 1080p, 2k, 4k |
kling-3.0-omni | 720p, 1080p, 2k, 4k |
minimax-h3 | 768p, 2k |
Note: The kling-3.0 family and gemini-omni-flash-preview do not support 480p. minimax-h3 only supports 768p and 2k.
Python example:
import requests
import time
API_KEY = "np_your_api_key_here"
BASE_URL = "https://api.useneospark.com/api/v1"
def create_video_task(prompt: str, **kwargs) -> dict:
"""Create a video generation task"""
payload = {
"prompt": prompt,
"model": kwargs.get("model", "seedance-2.0"),
"duration": kwargs.get("duration", 5),
"ratio": kwargs.get("ratio", "16:9"),
"resolution": kwargs.get("resolution", "720p"),
"generate_audio": kwargs.get("generate_audio", False),
"watermark": kwargs.get("watermark", False),
}
# Optional advanced parameters
for key in ["seed", "camera_fixed", "return_last_frame", "draft",
"frames", "fps", "service_tier", "tools"]:
if key in kwargs:
payload[key] = kwargs[key]
# Optional resource parameters
for key in ["first_frame_url", "first_frame_upload_id", "first_frame_path",
"last_frame_url", "last_frame_upload_id", "last_frame_path",
"reference_image_urls", "reference_image_upload_ids", "reference_image_paths",
"reference_video_urls", "reference_video_upload_ids", "reference_video_paths",
"reference_audio_urls", "reference_audio_upload_ids", "reference_audio_paths"]:
if key in kwargs:
payload[key] = kwargs[key]
response = requests.post(
f"{BASE_URL}/video/generations",
headers={
"X-API-Key": API_KEY,
"Content-Type": "application/json"
},
json=payload
)
response.raise_for_status()
return response.json()
# Example 1: Text-to-video
result = create_video_task(
prompt="A cute cat playing on the grass, sunshine and a gentle breeze",
model="seedance-2.0",
duration=5,
ratio="16:9",
resolution="720p"
)
print(f"Task created: {result['data']['task_id']}")
print(f"Estimated cost: {result['data']['pricing']['estimated_cost']} credits")
# Example 2: First-frame image-to-video
result = create_video_task(
prompt="The camera slowly pushes in, revealing more details",
model="seedance-2.0",
duration=5,
first_frame_url="https://example.com/image.png"
)
# Example 3: Multimodal reference (image style + audio rhythm)
result = create_video_task(
prompt="Generate a video following the style of the reference image and the rhythm of the audio",
model="seedance-2.0",
duration=10,
reference_image_urls=["https://example.com/style.png"],
reference_audio_urls=["https://example.com/music.mp3"]
)
JavaScript example:
async function createVideoTask(prompt, options = {}) {
const payload = {
prompt,
model: options.model || "seedance-2.0",
duration: options.duration || 5,
ratio: options.ratio || "16:9",
resolution: options.resolution || "720p",
generate_audio: options.generateAudio || false,
watermark: options.watermark || false,
...options,
};
const response = await fetch(`${BASE_URL}/video/generations`, {
method: "POST",
headers: {
"X-API-Key": API_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
// Usage example
createVideoTask("A cute cat playing on the grass, sunshine and a gentle breeze", {
model: "seedance-2.0",
duration: 5,
}).then((result) => {
console.log("Task ID:", result.data.task_id);
});
Response example:
{
"code": 200,
"data": {
"task_id": "video_a1b2c3d4e5f6",
"external_task_id": "cgt-20260513152306-y92dz",
"status": "pending",
"progress": 0,
"model": "seedance-2.0",
"duration": 5,
"ratio": "16:9",
"resolution": "720p",
"pricing": {
"estimated_cost": 110,
"currency": "credits"
},
"available_points_after": 23864,
"created_at": "2025-04-09T12:00:00"
}
}
2. Get Task Status
GET /api/v1/video/generations/{task_id}
Gets the current status of a video generation task.
Python example:
def get_video_task(task_id: str) -> dict:
"""Get video task status"""
response = requests.get(
f"{BASE_URL}/video/generations/{task_id}",
headers={"X-API-Key": API_KEY}
)
response.raise_for_status()
return response.json()
# Query the task
result = get_video_task("video_a1b2c3d4e5f6")
print(f"Status: {result['data']['status']}")
print(f"Progress: {result['data']['progress']}%")
if result["data"]["status"] == "completed":
print(f"Video URL: {result['data']['video_url']}")
JavaScript example:
async function getVideoTask(taskId) {
const response = await fetch(`${BASE_URL}/video/generations/${taskId}`, {
headers: { "X-API-Key": API_KEY },
});
return await response.json();
}
// Usage example
getVideoTask("video_a1b2c3d4e5f6").then((result) => {
console.log("Status:", result.data.status);
console.log("Progress:", result.data.progress);
});
Response fields:
task_id: Task IDexternal_task_id: Upstream provider task ID (e.g.cgt-xxx)status: Status,pending/processing/completed/failed/cancelledprogress: Progress percentageprompt: Promptmodel: Modelduration: Durationratio: Aspect ratioresolution: Resolutiongenerate_audio: Whether audio is generatedwatermark: Whether a watermark is addedvideo_url: Result video URL (a local/S3 URL is returned once completed)estimated_cost: Estimated costactual_cost: Actual charge (after completion)completion_tokens: Completion token usage reported by upstream (reference)total_tokens: Total token usage reported by upstream (reference)token_cost: Token-based cost (reference, already includes the 1.55x factor)error_msg: Error message (on failure)created_at: Creation timecompleted_at: Completion time
Notes:
- Each query automatically syncs the latest status from upstream.
- Once the task completes, the backend automatically downloads the video to local/S3 storage, and
video_urlreturns a locally accessible URL.
Response example (processing):
{
"code": 200,
"data": {
"task_id": "video_a1b2c3d4e5f6",
"external_task_id": "cgt-20260513152306-y92dz",
"status": "processing",
"progress": 45,
"prompt": "A cute cat...",
"model": "seedance-2.0",
"duration": 5,
"ratio": "16:9",
"resolution": "720p",
"generate_audio": false,
"watermark": false,
"video_url": null,
"estimated_cost": 110,
"actual_cost": null,
"error_msg": null,
"created_at": "2025-04-09T12:00:00",
"completed_at": null
}
}
Response example (completed):
{
"code": 200,
"data": {
"task_id": "video_a1b2c3d4e5f6",
"external_task_id": "cgt-20260513152306-y92dz",
"status": "completed",
"progress": 100,
"prompt": "A cute cat...",
"model": "seedance-2.0",
"duration": 5,
"ratio": "16:9",
"resolution": "720p",
"generate_audio": false,
"watermark": false,
"video_url": "/uploads/videos/1/video_a1b2c3d4e5f6.mp4",
"estimated_cost": 110,
"actual_cost": 110,
"error_msg": null,
"created_at": "2025-04-09T12:00:00",
"completed_at": "2025-04-09T12:02:30"
}
}
3. Poll Until Task Completion
Full Python example:
import time
import requests
def wait_for_video_completion(
task_id: str,
poll_interval: int = 5,
max_attempts: int = 60,
max_network_retries: int = 3,
) -> dict:
"""
Poll until video generation completes
Args:
task_id: Task ID
poll_interval: Polling interval in seconds, default 5
max_attempts: Maximum number of polls, default 60 (5 minutes)
max_network_retries: Maximum retries per poll on network/5xx errors
Returns:
The completed task data
"""
network_retry = 0
for attempt in range(max_attempts):
try:
result = get_video_task(task_id)
network_retry = 0 # Reset the network retry counter on success
except requests.exceptions.RequestException as e:
# Retry transient errors (network glitches, timeouts, 5xx) instead of failing immediately
network_retry += 1
if network_retry > max_network_retries:
raise Exception(f"Failed to poll task status: {e}")
time.sleep(poll_interval)
continue
data = result["data"]
status = data["status"]
progress = data["progress"]
print(f"[{attempt + 1}/{max_attempts}] Status: {status}, Progress: {progress}%")
if status == "completed":
# Some upstream providers return completed first and generate video_url
# asynchronously; keep polling briefly
if not data.get("video_url"):
time.sleep(poll_interval)
continue
print(f"Video completed! URL: {data['video_url']}")
return data
elif status == "failed":
error_msg = data.get("error_msg", "Unknown error")
raise Exception(f"Video generation failed: {error_msg}")
elif status == "cancelled":
raise Exception("Video generation was cancelled")
time.sleep(poll_interval)
raise TimeoutError(f"Video generation timeout after {max_attempts * poll_interval} seconds")
# Full flow: create and wait for completion
def generate_video(prompt: str, **options) -> str:
"""Full video generation flow; returns the video URL"""
# 1. Create the task
print("Creating video task...")
result = create_video_task(prompt, **options)
task_id = result["data"]["task_id"]
print(f"Task created: {task_id}")
# 2. Wait for completion
print("Waiting for video generation...")
final_data = wait_for_video_completion(task_id)
return final_data["video_url"]
# Usage example
if __name__ == "__main__":
try:
video_url = generate_video(
prompt="A cute corgi running on the beach, golden sand under the setting sun",
model="seedance-2.0",
duration=5,
ratio="16:9",
resolution="720p"
)
print(f"\nSuccess! Video URL: {video_url}")
except Exception as e:
print(f"\nError: {e}")
Full JavaScript/TypeScript example:
interface VideoTask {
task_id: string;
status: "pending" | "processing" | "completed" | "failed" | "cancelled";
progress: number;
video_url?: string;
error_msg?: string;
}
interface VideoOptions {
model?: string;
duration?: number;
ratio?: string;
resolution?: string;
generateAudio?: boolean;
watermark?: boolean;
seed?: number;
cameraFixed?: boolean;
returnLastFrame?: boolean;
draft?: boolean;
frames?: number;
fps?: number;
serviceTier?: string;
tools?: Array<{type: string}>;
firstFrameUrl?: string;
firstFrameUploadId?: string;
lastFrameUrl?: string;
lastFrameUploadId?: string;
referenceImageUrls?: string[];
referenceVideoUrls?: string[];
referenceAudioUrls?: string[];
}
class NeoSparkVideoClient {
private apiKey: string;
private baseUrl: string;
constructor(apiKey: string, baseUrl = "https://api.useneospark.com/api/v1") {
this.apiKey = apiKey;
this.baseUrl = baseUrl;
}
private async request(
endpoint: string,
options: RequestInit = {}
): Promise<any> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...options,
headers: {
"X-API-Key": this.apiKey,
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || `HTTP ${response.status}`);
}
return await response.json();
}
async createTask(
prompt: string,
options: VideoOptions = {}
): Promise<VideoTask> {
const result = await this.request("/video/generations", {
method: "POST",
body: JSON.stringify({
prompt,
model: options.model || "seedance-2.0",
duration: options.duration || 5,
ratio: options.ratio || "16:9",
resolution: options.resolution || "720p",
generate_audio: options.generateAudio || false,
watermark: options.watermark || false,
seed: options.seed,
camera_fixed: options.cameraFixed || false,
return_last_frame: options.returnLastFrame || false,
draft: options.draft || false,
frames: options.frames,
fps: options.fps,
service_tier: options.serviceTier || "default",
tools: options.tools,
first_frame_url: options.firstFrameUrl,
first_frame_upload_id: options.firstFrameUploadId,
last_frame_url: options.lastFrameUrl,
last_frame_upload_id: options.lastFrameUploadId,
reference_image_urls: options.referenceImageUrls,
reference_video_urls: options.referenceVideoUrls,
reference_audio_urls: options.referenceAudioUrls,
}),
});
return result.data;
}
async getTask(taskId: string): Promise<VideoTask> {
const result = await this.request(`/video/generations/${taskId}`);
return result.data;
}
async waitForCompletion(
taskId: string,
onProgress?: (status: string, progress: number) => void,
pollInterval: number = 5000,
maxNetworkRetries: number = 3
): Promise<VideoTask> {
let networkRetry = 0;
return new Promise((resolve, reject) => {
const poll = async () => {
try {
const task = await this.getTask(taskId);
networkRetry = 0; // Reset on success
if (onProgress) {
onProgress(task.status, task.progress);
}
if (task.status === "completed") {
// Some upstream providers return completed first and generate video_url asynchronously
if (!task.video_url) {
setTimeout(poll, pollInterval);
return;
}
resolve(task);
} else if (task.status === "failed") {
reject(new Error(`Video generation failed: ${task.error_msg}`));
} else if (task.status === "cancelled") {
reject(new Error("Video generation was cancelled"));
} else {
setTimeout(poll, pollInterval);
}
} catch (error: any) {
// Retry transient errors first: network glitches, timeouts, 5xx, etc.
networkRetry += 1;
if (networkRetry > maxNetworkRetries) {
reject(error);
return;
}
setTimeout(poll, pollInterval);
}
};
poll();
});
}
async generateVideo(
prompt: string,
options: VideoOptions = {},
onProgress?: (status: string, progress: number) => void
): Promise<string> {
// Create the task
const task = await this.createTask(prompt, options);
console.log("Task created:", task.task_id);
// Wait for completion
const completed = await this.waitForCompletion(task.task_id, onProgress);
if (!completed.video_url) {
throw new Error("Video URL not found in completed task");
}
return completed.video_url;
}
}
// React Hook example
import { useState, useCallback } from "react";
export function useVideoGeneration(apiKey: string) {
const [status, setStatus] = useState<string>("idle");
const [progress, setProgress] = useState<number>(0);
const [videoUrl, setVideoUrl] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const generateVideo = useCallback(
async (prompt: string, options?: VideoOptions) => {
setStatus("creating");
setProgress(0);
setError(null);
const client = new NeoSparkVideoClient(apiKey);
try {
const url = await client.generateVideo(
prompt,
options,
(status, progress) => {
setStatus(status);
setProgress(progress);
}
);
setVideoUrl(url);
setStatus("completed");
return url;
} catch (err: any) {
setError(err.message);
setStatus("failed");
throw err;
}
},
[apiKey]
);
return { generateVideo, status, progress, videoUrl, error };
}
// React component usage example
function VideoGenerator() {
const { generateVideo, status, progress, videoUrl, error } =
useVideoGeneration("np_your_api_key");
const [prompt, setPrompt] = useState("");
const handleGenerate = async () => {
try {
await generateVideo(prompt, { duration: 5, ratio: "16:9" });
} catch (e) {
console.error("Failed to generate video:", e);
}
};
return (
<div>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Describe the video you want to generate..."
/>
<button onClick={handleGenerate} disabled={status === "processing"}>
{status === "processing"
? `Generating... ${progress}%`
: "Generate Video"}
</button>
{error && <div className="error">{error}</div>}
{videoUrl && <video src={videoUrl} controls width="100%" />}
</div>
);
}
4. List All Video Tasks
GET /api/v1/video/generations
Gets the list of video tasks for the current user.
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| page | int | No | Page number, default 1 |
| page_size | int | No | Items per page, default 20, maximum 100 |
| status | string | No | Status filter: pending/processing/completed/failed/cancelled |
Python example:
def list_video_tasks(page: int = 1, page_size: int = 20, status: str = None) -> dict:
"""List video tasks"""
params = {
"page": page,
"page_size": page_size
}
if status:
params["status"] = status
response = requests.get(
f"{BASE_URL}/video/generations",
headers={"X-API-Key": API_KEY},
params=params
)
return response.json()
# Get all tasks
result = list_video_tasks()
for task in result["data"]["items"]:
print(f"{task['task_id']}: {task['status']} - {task['prompt'][:30]}...")
# Get completed tasks
completed = list_video_tasks(status="completed")
print(f"Completed tasks: {completed['data']['total']}")
Response example:
{
"code": 200,
"data": {
"items": [
{
"task_id": "video_a1b2c3d4e5f6",
"status": "completed",
"progress": 100,
"prompt": "A cute cat...",
"model": "seedance-2.0",
"preview_url": "/uploads/videos/1/video_a1b2c3d4e5f6.mp4",
"estimated_cost": 110,
"error_msg": null,
"created_at": "2025-04-09T12:00:00"
}
],
"total": 1,
"page": 1,
"page_size": 20
}
}
5. Delete a Video Task
DELETE /api/v1/video/generations/{task_id}
Deletes the record of a completed video task.
Python example:
def delete_video_task(task_id: str) -> dict:
"""Delete a video task"""
response = requests.delete(
f"{BASE_URL}/video/generations/{task_id}",
headers={"X-API-Key": API_KEY}
)
return response.json()
# Delete the task
result = delete_video_task("video_a1b2c3d4e5f6")
print(result["message"]) # "Task deleted"
Note: Only completed, failed, or cancelled tasks can be deleted.
6. List Models
GET /api/v1/video/models
Gets the list of supported video generation models and their pricing.
Python example:
def list_video_models() -> dict:
"""Get the list of video models"""
response = requests.get(
f"{BASE_URL}/video/models",
headers={"X-API-Key": API_KEY}
)
return response.json()
# Get the model list
result = list_video_models()
for model in result["data"]["models"]:
print(f"{model['name']}: {model['price_per_second']} credits/second")
Response example:
{
"code": 200,
"data": {
"models": [
{
"id": "seedance-2.0",
"name": "Seedance 2.0",
"description": "Highest quality, balanced cost and generation speed",
"price_per_second": 22
},
{
"id": "seedance-2.0-fast",
"name": "Seedance 2.0 Fast",
"description": "Faster generation, lower cost, slightly lower quality",
"price_per_second": 18
}
],
"ratios": ["16:9", "4:3", "1:1", "3:4", "9:16", "21:9", "adaptive"],
"resolutions": {
"seedance-2.0": ["480p", "720p", "1080p"],
"seedance-2.0-fast": ["480p", "720p"]
},
"durations": {"min": 4, "max": 15, "default": 5},
"capabilities": [
"text_to_video",
"image_to_video",
"multi_modal_reference",
"video_editing",
"video_extension",
"audio_generation",
"web_search"
]
}
}
Assets API
To use real-person mode or reuse assets in video generation, you can first register images/videos/audios in the Moyu asset library, then reference the returned asset_id in generation requests.
1. Upload an Asset Directly
POST /api/v1/video/assets
Uploads a file directly and registers it as an asset.
Request body: multipart/form-data
file: File contentasset_type: Asset type,image/video/audio, defaultimagename: Asset display name (optional)
Response example:
{
"code": 200,
"data": {
"asset_id": "asset_abc123",
"name": "product.jpg",
"asset_type": "Image",
"status": "active",
"url": "https://...",
"upload_id": "up_xxx",
"local_url": "/uploads/..."
}
}
Notes:
- Image assets must be at least 300×300 pixels.
- After upload, the API waits for the asset status to become
Active, up to 60 seconds. On timeout it returnsstatus: processing; pollGET /video/assets/{asset_id}yourself in that case.
2. Create an Asset from an Existing Upload
POST /api/v1/video/assets/from-upload
Registers a file previously uploaded via /storage/upload as a Moyu asset library asset.
Request body:
{
"upload_id": "up_abc123",
"asset_type": "image",
"name": "product"
}
Response example: Same as uploading an asset directly.
3. Get Asset Status
GET /api/v1/video/assets/{asset_id}
Gets the current status of an asset.
Response example:
{
"code": 200,
"data": {
"asset_id": "asset_abc123",
"name": "product.jpg",
"asset_type": "Image",
"status": "active",
"url": "https://..."
}
}
Video Watermark Removal API
Uses the WaveSpeed AI video-watermark-remover model to remove watermarks, logos, or subtitles from videos.
1. Create a Watermark Removal Task
POST /api/v1/video/remove-watermark
Request parameters:
| Field | Type | Required | Description |
|---|---|---|---|
| video_url | string | No (choose one of three) | Public video URL |
| video_upload_id | string | No (choose one of three) | Video upload ID |
| video_path | string | No (choose one of three) | Local video path, e.g. /uploads/... |
| duration | int | No | Video duration in seconds, 1-600. Auto-detected if omitted |
Billing: 1 credit/second, minimum 5 credits.
Response example:
{
"code": 200,
"data": {
"task_id": "vwm_a1b2c3d4e5f6",
"external_task_id": "wavespeed_xxx",
"status": "pending",
"progress": 0,
"video_duration": 10,
"pricing": {
"estimated_cost": 10,
"currency": "credits",
"price_per_second": 1,
"minimum_charge_seconds": 5
},
"available_points_after": 23864,
"created_at": "2025-04-09T12:00:00"
}
}
2. Get Watermark Removal Task Status
GET /api/v1/video/remove-watermark/{task_id}
Response example (completed):
{
"code": 200,
"data": {
"task_id": "vwm_a1b2c3d4e5f6",
"external_task_id": "wavespeed_xxx",
"status": "completed",
"progress": 100,
"source_video_url": "https://...",
"video_duration": 10,
"result_video_url": "/uploads/videos/1/vwm_a1b2c3d4e5f6.mp4",
"estimated_cost": 10,
"actual_cost": 10,
"error_msg": null,
"created_at": "2025-04-09T12:00:00",
"completed_at": "2025-04-09T12:02:30"
}
}
3. List Watermark Removal Tasks
GET /api/v1/video/remove-watermark
Query parameters:
page: Page number, default 1page_size: Items per page, default 20status: Status filter
Error Handling
Common Error Codes
| HTTP Code | Description | Recommendation |
|---|---|---|
| 200 | Success | - |
| 400 | Invalid request parameters | Check parameter formats and ranges |
| 401 | Invalid or expired API key | Check the API key and regenerate it |
| 402 | Insufficient credits | Check your balance; top up or redeem credits |
| 403 | User is restricted from generating videos | An administrator set can_generate_video=false |
| 404 | Task not found | Check whether the task_id is correct; deleted tasks also return 404 |
| 429 | Too many requests | Reduce request frequency and add retry backoff |
| 500 | Internal server error | Retry later; contact support |
| 502 | External service error | Upstream service temporarily unavailable; retry later |
Handling Polling Failures
While polling GET /video/generations/{task_id}, you may hit network glitches, timeouts, or transient upstream 5xx errors. Recommendations:
-
Distinguish error types:
- Network / timeout / 5xx: Retry 3-5 times with backoff (e.g. 5s → 7.5s → 11s).
- 401 / 403: The API key is invalid or the user is restricted; stop polling and prompt for re-authentication.
- 404: The task does not exist or was deleted; stop polling.
- Task status
failed/cancelled: A real failure; displayerror_msg.
-
completedbut novideo_url: Some upstream providers mark the task completed first and generate the final URL asynchronously. Keep polling a few more times instead of reporting an error immediately. -
Set a per-request timeout: Set a 10-20 second timeout on each polling request to avoid connections hanging for a long time.
Error Response Format
Business errors (4xx):
{
"code": 402,
"message": "Insufficient credits to complete this operation. Available credits: 50, required: 110",
"data": null
}
External service errors (502):
{
"code": 502,
"message": "Video task creation failed: Server error '503 Service Unavailable' ...",
"error": "Server error '503 Service Unavailable' ...",
"traceback": "...",
"error_type": "task_creation_failed"
}
Possible error_type values:
task_creation_failed— Task creation failedauthentication_failed— Upstream authentication failed (check MOYU_API_KEY / TENCENT_VOD_SECRET_ID/SECRET_KEY / GOOGLE_APPLICATION_CREDENTIALS)asset_upload_failed— Legacy asset upload failed (deprecated)asset_registration_failed— Asset registration with Moyu failedasset_processing_failed— Asset processing failedomni_task_creation_failed— Omni task creation failedvertex_video_task_creation_failed— Vertex AI Gemini Omni Flash task creation failedtencent_vod_task_creation_failed— Tencent Cloud VOD task creation failedwavespeed_task_creation_failed— WaveSpeed watermark removal task creation failedtimeout— Upstream service timeoutinternal_server_error— Internal server error
Python Error Handling Example
import requests
from requests.exceptions import HTTPError
def safe_api_call(func):
"""Decorator for API calls with unified error handling"""
def wrapper(*args, **kwargs):
try:
result = func(*args, **kwargs)
# Check business error codes
code = result.get("code", 200)
if code != 200:
message = result.get("message", "Unknown error")
if code == 401:
raise AuthenticationError(f"API Key invalid: {message}")
elif code == 402:
raise InsufficientPointsError(f"Not enough credits: {message}")
else:
raise APIError(f"Error {code}: {message}")
return result
except HTTPError as e:
status_code = e.response.status_code
if status_code == 401:
raise AuthenticationError("API Key is invalid or expired")
else:
raise APIError(f"HTTP {status_code}: {str(e)}")
except requests.exceptions.RequestException as e:
raise NetworkError(f"Network error: {str(e)}")
return wrapper
class AuthenticationError(Exception):
pass
class InsufficientPointsError(Exception):
pass
class APIError(Exception):
pass
class NetworkError(Exception):
pass
# Using the decorator
@safe_api_call
def create_video_task_safe(prompt: str, **kwargs) -> dict:
payload = {
"prompt": prompt,
"model": kwargs.get("model", "seedance-2.0"),
"duration": kwargs.get("duration", 5),
"ratio": kwargs.get("ratio", "16:9"),
"resolution": kwargs.get("resolution", "720p"),
}
response = requests.post(
f"{BASE_URL}/video/generations",
headers={
"X-API-Key": API_KEY,
"Content-Type": "application/json"
},
json=payload
)
response.raise_for_status()
return response.json()
Best Practices
1. API Key Security
❌ Don’t:
// Hardcoded in frontend code
const API_KEY = "np_xxxxxxxxxxxxx";
✅ Recommended:
// Read from environment variables
const API_KEY = process.env.NEOSPARK_API_KEY;
// Or use a config file (.gitignore)
import config from "./config";
const API_KEY = config.apiKey;
2. Credit Estimation
Before generating a video, check the user’s credits:
def check_points_before_task(cost: int) -> bool:
"""Check whether there are enough credits"""
response = requests.get(
f"{BASE_URL}/auth/me",
headers={"X-API-Key": API_KEY}
)
data = response.json()
balance = data["data"]["balance"]
if balance < cost:
print(f"Insufficient points: {balance} < {cost}")
return False
return True
# Video price table (base unit price, credits/second)
VIDEO_PRICES = {
"seedance-2.0": 22,
"seedance-2.0-fast": 18,
"kling-3.0": 15,
"kling-3.0-omni": 18,
}
def calculate_video_cost(model: str, duration: int) -> int:
"""Calculate the actual charge for video generation (includes the 0.79 discount compensation)"""
base = VIDEO_PRICES.get(model, 22) * duration
return (base * 100 + 78) // 79 # ceil(base / 0.79)
3. Robust Polling
Video generation takes a while, and the polling phase is prone to network glitches. Do not declare a task failed just because one query failed. Recommended strategy:
- Normal
pending/processing: Poll every 3-5 seconds. - When a query fails:
- Network / timeout / 5xx: Retry 3-5 times with exponential backoff.
- 401 / 403: Stop polling and re-authenticate.
- 404: Stop polling; the task may have been deleted.
completedbut novideo_url: Keep polling for 5-10 seconds while the URL becomes ready.- Set a per-query timeout (10-20 seconds) to avoid the browser/client hanging for a long time.
4. Task Management
Keep a local copy of the task list so users can review their history:
import json
import os
from datetime import datetime
class TaskManager:
def __init__(self, storage_file: str = "video_tasks.json"):
self.storage_file = storage_file
self.tasks = self._load_tasks()
def _load_tasks(self) -> dict:
if os.path.exists(self.storage_file):
with open(self.storage_file, 'r') as f:
return json.load(f)
return {}
def _save_tasks(self):
with open(self.storage_file, 'w') as f:
json.dump(self.tasks, f, indent=2)
def add_task(self, task_id: str, prompt: str, **metadata):
self.tasks[task_id] = {
"task_id": task_id,
"prompt": prompt,
"status": "pending",
"created_at": datetime.now().isoformat(),
**metadata
}
self._save_tasks()
def update_task(self, task_id: str, **updates):
if task_id in self.tasks:
self.tasks[task_id].update(updates)
self._save_tasks()
def get_task(self, task_id: str) -> dict:
return self.tasks.get(task_id)
def list_tasks(self, status: str = None) -> list:
tasks = list(self.tasks.values())
if status:
tasks = [t for t in tasks if t["status"] == status]
return sorted(tasks, key=lambda x: x["created_at"], reverse=True)
5. Complete Example Project Structure
my-video-app/
├── .env # Environment variables (not committed to Git)
├── config.py # Configuration management
├── neospark_client.py # NeoSpark client wrapper
├── task_manager.py # Task management
├── video_generator.py # Video generation logic
└── main.py # Main program
config.py:
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
NEOSPARK_API_KEY = os.getenv("NEOSPARK_API_KEY")
NEOSPARK_BASE_URL = os.getenv("NEOSPARK_BASE_URL", "https://api.useneospark.com/api/v1")
DEFAULT_VIDEO_MODEL = "seedance-2.0"
DEFAULT_VIDEO_DURATION = 5
DEFAULT_VIDEO_RATIO = "16:9"
DEFAULT_VIDEO_RESOLUTION = "720p"
@classmethod
def validate(cls):
if not cls.NEOSPARK_API_KEY:
raise ValueError("NEOSPARK_API_KEY is required")
Config.validate()
Reference Links
- API Documentation: https://api.useneospark.com/docs
- Admin Console: https://platform.useneospark.com/user
- Technical Support: alex.zhang@useneospark.com
Document Version: 2.3 Last Updated: 2026-07-31 Compatible Versions: seedance-2.0 / seedance-2.0-fast / gemini-omni-flash-preview / kling-3.0 / kling-3.0-omni