🎬 Seedance 2.5 Global API is now live
No real-person verification required No queues - instant generation 🆕 Seedance 2.5 model now live 🆕 MiniMax H3 model now live 🆕 Wan 3.0 model now live Welcome to try it now → No real-person verification required No queues - instant generation 🆕 Seedance 2.5 model now live 🆕 MiniMax H3 model now live 🆕 Wan 3.0 model now live Welcome to try it now →

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

  1. Quick Start
  2. Get an API Key
  3. Authentication
  4. Complete Video Generation Workflow
  5. Supported Models and Pricing
  6. Video Generation API Reference
  7. Assets API
  8. Video Watermark Removal API
  9. Error Handling
  10. Best Practices
  11. 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

  1. Visit https://platform.useneospark.com/user
  2. Sign in with your Google account
  3. Go to “User” → “API Keys”
  4. Click “Create API Key”
  5. 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:

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:

ModelDescriptionSupported ResolutionsMax DurationStatus
seedance-2.0High quality, balanced quality and speed, supports 4K480p, 720p, 1080p, 4k15sAvailable
seedance-2.0-fastFast generation, higher speed, up to 720p480p, 720p15sAvailable
seedance-2.5Even higher quality, supports 4K480p, 720p, 1080p, 4k30sAvailable
gemini-omni-flash-previewGoogle Vertex AI short video generation with native audio output720p15sAvailable
kling-3.0Tencent Cloud VOD Kling 3.0720p, 1080p, 2k, 4k15sAvailable
kling-3.0-omniTencent Cloud VOD Kling 3.0 Omni multimodal edition720p, 1080p, 2k, 4k15sAvailable
minimax-h3Next-generation open general-purpose multimodal video model; supports text-to-video, image-to-video, first/last frame, and multimodal references768p, 2k15sAvailable
wan3.0-videoAlibaba Cloud DashScope Wan 3.0; supports text-to-video, image-to-video, video-to-video, and reference audio480p, 720p, 1080p30sAvailable

Naming note: Internally, the code recognizes doubao-seedance-2-0-260128 / doubao-seedance-2-0-fast-260128 / doubao-seedance-2.5 as aliases of seedance-2.0 / seedance-2.0-fast / seedance-2.5, but the public API always uses seedance-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 is MiniMax-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.0 720p baseline: about $0.084/second (no audio) / $0.126/second (with audio)
  • kling-3.0-omni adds roughly a 20% premium on top of kling-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/second
  • 720p: $0.08/second → about 27 credits/second
  • 1080p: $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.79 is 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_cost fields 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 completed and video_url directly
  • Tencent Cloud Kling:
    • Does not support 480p
    • Upstream statuses: PENDING / PROCESSING / FINISH / FAIL
  • 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 Active status before use (wait up to 120 seconds)
  • MiniMax-H3:
    • Only supports 768p and 2k resolutions
    • Maximum 15 seconds
    • Supports the adaptive aspect ratio (auto-fit)
    • Up to 9 reference images, 3 reference videos, and 3 reference audios
    • Does not support face_swap_mode or real_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
  • Wan 3.0:
    • Only supports 480p, 720p, 1080p
    • Maximum 30 seconds
    • Supports the adaptive aspect 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, or watermark
    • The backend compresses image assets to JPEG and converts them to base64 data URIs before submission
    • Upstream statuses: PENDING / RUNNING / SUCCEEDED / FAILED / CANCELLED

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):

  • seed
  • camera_fixed
  • return_last_frame
  • draft
  • frames
  • fps
  • service_tier
  • tools
  • reference_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:

FieldTypeRequiredDescription
promptstringYesVideo description text, 1-2000 characters
modelstringNoModel 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
durationintNoVideo duration in seconds. Default 5. Seedance 2.0/2.0-fast/Kling/Omni/MiniMax-H3 support 415; Seedance 2.5 and Wan 3.0 support 430
ratiostringNoAspect ratio, default 16:9. MiniMax-H3 additionally supports adaptive
resolutionstringNoResolution, default 720p. Supported ranges differ per model; see “Supported Models and Pricing”
generate_audioboolNoWhether to generate audio, default false
watermarkboolNoWhether to add a watermark, default false
real_person_modeboolNoWhether to enable real-person mode, default false (requires uploading assets via /video/assets first and waiting for Active status)
face_swap_modeboolNoWhether to enable precision face-swap mode (Seedance family only), default false. When enabled, provide 1 reference image + 1 reference video
negative_promptstringNoNegative prompt. In face_swap_mode it is passed through to the Seedance metadata; in normal mode its effect depends on upstream support
seedintNoRandom seed (not supported by the current gateway; kept for compatibility)
camera_fixedboolNoWhether to fix the camera position (not supported by the current gateway; kept for compatibility)
return_last_frameboolNoWhether to return the last frame image (not supported by the current gateway; kept for compatibility)
draftboolNoDraft mode (not supported by the current gateway; kept for compatibility)
framesintNoTotal number of video frames (not supported by the current gateway; kept for compatibility)
fpsintNoFrame rate (not supported by the current gateway; kept for compatibility)
service_tierstringNoService tier (not supported by the current gateway; kept for compatibility)
toolsarrayNoTool 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 via POST /video/assets or POST /video/assets/from-upload, and wait until the asset status becomes Active before referencing it.

FieldTypeDescription
first_frame_url / first_frame_upload_id / first_frame_path / first_frame_asset_idstringFirst frame image
last_frame_url / last_frame_upload_id / last_frame_path / last_frame_asset_idstringLast frame image
reference_image_urls / reference_image_upload_ids / reference_image_paths / reference_image_asset_idsarrayReference image list. Up to 9 for Seedance, up to 5 for Omni
reference_video_urls / reference_video_upload_ids / reference_video_pathsarrayReference video list, up to 3
reference_audio_urls / reference_audio_upload_ids / reference_audio_pathsarrayReference audio list (only supported by Wan 3.0)

Mixing rules: reference_image cannot be used together with first_frame / last_frame (normal mode, Seedance family). gemini-omni-flash-preview, minimax-h3, and wan3.0-video allow 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 4K
  • seedance-2.0-fast - Fast generation, up to 720p
  • seedance-2.5 - Even higher quality, supports 4K and up to 30 seconds
  • gemini-omni-flash-preview - Google Vertex AI; supports text-to-video / image-to-video and native audio; 720p only
  • kling-3.0 - Tencent Cloud VOD Kling 3.0
  • kling-3.0-omni - Kling 3.0 Omni multimodal edition
  • minimax-h3 - Next-generation open general-purpose multimodal video model; supports 768p/2k, text-to-video / image-to-video / first-last frame / multimodal references
  • wan3.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 - Portrait
  • 1:1 - Square
  • 4:3, 3:4, 21:9, adaptive

Supported resolutions:

ModelSupported Resolutions
seedance-2.0480p, 720p, 1080p, 4k
seedance-2.0-fast480p, 720p
gemini-omni-flash-preview720p
kling-3.0720p, 1080p, 2k, 4k
kling-3.0-omni720p, 1080p, 2k, 4k
minimax-h3768p, 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 ID
  • external_task_id: Upstream provider task ID (e.g. cgt-xxx)
  • status: Status, pending / processing / completed / failed / cancelled
  • progress: Progress percentage
  • prompt: Prompt
  • model: Model
  • duration: Duration
  • ratio: Aspect ratio
  • resolution: Resolution
  • generate_audio: Whether audio is generated
  • watermark: Whether a watermark is added
  • video_url: Result video URL (a local/S3 URL is returned once completed)
  • estimated_cost: Estimated cost
  • actual_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 time
  • completed_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_url returns 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:

ParameterTypeRequiredDescription
pageintNoPage number, default 1
page_sizeintNoItems per page, default 20, maximum 100
statusstringNoStatus 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 content
  • asset_type: Asset type, image / video / audio, default image
  • name: 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 returns status: processing; poll GET /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:

FieldTypeRequiredDescription
video_urlstringNo (choose one of three)Public video URL
video_upload_idstringNo (choose one of three)Video upload ID
video_pathstringNo (choose one of three)Local video path, e.g. /uploads/...
durationintNoVideo 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 1
  • page_size: Items per page, default 20
  • status: Status filter

Error Handling

Common Error Codes

HTTP CodeDescriptionRecommendation
200Success-
400Invalid request parametersCheck parameter formats and ranges
401Invalid or expired API keyCheck the API key and regenerate it
402Insufficient creditsCheck your balance; top up or redeem credits
403User is restricted from generating videosAn administrator set can_generate_video=false
404Task not foundCheck whether the task_id is correct; deleted tasks also return 404
429Too many requestsReduce request frequency and add retry backoff
500Internal server errorRetry later; contact support
502External service errorUpstream 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:

  1. 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; display error_msg.
  2. completed but no video_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.

  3. 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 failed
  • authentication_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 failed
  • asset_processing_failed — Asset processing failed
  • omni_task_creation_failed — Omni task creation failed
  • vertex_video_task_creation_failed — Vertex AI Gemini Omni Flash task creation failed
  • tencent_vod_task_creation_failed — Tencent Cloud VOD task creation failed
  • wavespeed_task_creation_failed — WaveSpeed watermark removal task creation failed
  • timeout — Upstream service timeout
  • internal_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.
  • completed but no video_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()


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