WaveSpeedAI

Sora 2 to Seedance 2.5, Wan 3.0, or MiniMax H3: The Request Bodies and Python Code

Exact request bodies for moving Sora 2 text- and image-to-video calls to Seedance 2.5, Wan 3.0, and MiniMax H3, plus a size mapper and a Python submit-and-poll client.

By WaveSpeedAI7 min read

This is the code-level companion to Sora 2 API shuts down September 24: migrate to Seedance 2.5, Wan 3.0, or MiniMax H3. That guide explains why and which; this one shows the exact request bodies and a Python client. Every field name and enum value below is taken from the live WaveSpeedAI schemas on September 5, 2026. If a field is not shown, the endpoint does not have it.

The short version: replace size with aspect_ratio plus resolution, keep prompt and duration, keep image for image-to-video, and change the model ID. Nothing else in your client has to move.

The Sora 2 request you are replacing

Text-to-video on openai/sora-2/text-to-video:

{
  "prompt": "A red kite rising over a windy beach at sunset, handheld camera, waves audible",
  "size": "1280*720",
  "duration": 8
}

size is one of 720*1280 (default) or 1280*720. duration is one of 4, 8, 12, 16, or 20. Image-to-video on openai/sora-2/image-to-video drops size entirely and adds image:

{
  "prompt": "The kite lifts off and climbs, camera tilts up to follow",
  "image": "https://example.com/kite-frame.jpg",
  "duration": 8
}

Both bill $0.10 per second.

Seedance 2.5 Turbo

bytedance/seedance-2.5/text-to-video-turbo:

{
  "prompt": "A red kite rising over a windy beach at sunset, handheld camera, waves audible",
  "aspect_ratio": "16:9",
  "resolution": "720p",
  "duration": 8,
  "generate_audio": true
}
  • aspect_ratio: 16:9, 9:16, 4:3, 3:4, 1:1, 21:9. Default 16:9.
  • resolution: 720p or 1080p. Default 720p.
  • duration: integer 4 to 30. Default 5.
  • generate_audio: boolean, default true.
  • Optional: reference_images, reference_videos, reference_audios (arrays of URLs), which Sora had no equivalent for.

bytedance/seedance-2.5/image-to-video-turbo:

{
  "prompt": "The kite lifts off and climbs, camera tilts up to follow",
  "image": "https://example.com/kite-frame.jpg",
  "resolution": "720p",
  "duration": 8,
  "generate_audio": true
}

There is no aspect_ratio on image-to-video; the frame follows the image. last_image is optional for first-to-last-frame interpolation. Price at 720p is $0.20 per second, at 1080p $0.22.

Wan 3.0

alibaba/wan-3.0/text-to-video:

{
  "prompt": "A red kite rising over a windy beach at sunset, handheld camera, waves audible",
  "aspect_ratio": "16:9",
  "resolution": "720p",
  "duration": 8,
  "enable_audio": true,
  "enable_prompt_expansion": false,
  "seed": -1
}
  • aspect_ratio: 16:9, 9:16, 1:1, 4:3, 3:4. Default 16:9.
  • resolution: 480p, 720p, 1080p. Default 720p.
  • duration: integer 2 to 30. Default 5.
  • enable_audio: boolean, default true. enable_prompt_expansion: boolean, default false. seed: integer, -1 for random.

alibaba/wan-3.0/image-to-video:

{
  "prompt": "The kite lifts off and climbs, camera tilts up to follow",
  "image": "https://example.com/kite-frame.jpg",
  "resolution": "720p",
  "duration": 8,
  "enable_audio": true
}

Wan is the one image-to-video endpoint that still accepts an optional aspect_ratio; omit it and the output adapts to the image. last_image is optional. Price is $0.05 per second at 480p, $0.10 at 720p (identical to Sora 2), $0.20 at 1080p.

MiniMax H3

wavespeed-ai/minimax-h3/text-to-video:

{
  "prompt": "A red kite rising over a windy beach at sunset, handheld camera, waves audible",
  "aspect_ratio": "16:9",
  "resolution": "768p",
  "duration": 8,
  "seed": -1
}
  • aspect_ratio: 16:9, 9:16, 1:1, 4:3, 3:4, 21:9, 9:21. Default 16:9.
  • resolution: 480p, 540p, 768p, 1080p. Default 480p, so set it explicitly; 768p is the native canvas and the closest match to Sora’s 720p.
  • duration: integer 3 to 15. Default 5.
  • No audio field. Audio is always generated, and the prompt can describe the soundtrack.

wavespeed-ai/minimax-h3/image-to-video:

{
  "prompt": "The kite lifts off and climbs, camera tilts up to follow",
  "image": "https://example.com/kite-frame.jpg",
  "resolution": "768p",
  "duration": 8
}

No aspect_ratio; the canvas follows the image. last_image is optional. Price per second: $0.04 at 480p, $0.06 at 540p, $0.08 at 768p, $0.16 at 1080p.

Field mapping summary

Sora 2Seedance 2.5 TurboWan 3.0MiniMax H3
promptpromptpromptprompt
sizeaspect_ratio + resolutionaspect_ratio + resolutionaspect_ratio + resolution
duration 4, 8, 12, 16, 204 to 302 to 303 to 15
imageimage (+ last_image)image (+ last_image)image (+ last_image)
audio, always ongenerate_audio, default trueenable_audio, default truealways on
no seedno seedseedseed

A size to aspect_ratio and resolution helper

The one piece of logic that has to be written once. It covers the two Sora 2 sizes and the two Sora 2 Pro 1080p sizes, clamps duration into each model’s range, and drops fields an endpoint does not accept.

SIZE_MAP = {
    "1280*720":  ("16:9", "720p"),
    "720*1280":  ("9:16", "720p"),
    "1920*1080": ("16:9", "1080p"),
    "1080*1920": ("9:16", "1080p"),
}

TARGETS = {
    "seedance": {
        "t2v": "bytedance/seedance-2.5/text-to-video-turbo",
        "i2v": "bytedance/seedance-2.5/image-to-video-turbo",
        "duration": (4, 30),
        "res": {"720p": "720p", "1080p": "1080p"},
    },
    "wan": {
        "t2v": "alibaba/wan-3.0/text-to-video",
        "i2v": "alibaba/wan-3.0/image-to-video",
        "duration": (2, 30),
        "res": {"720p": "720p", "1080p": "1080p"},
    },
    "h3": {
        "t2v": "wavespeed-ai/minimax-h3/text-to-video",
        "i2v": "wavespeed-ai/minimax-h3/image-to-video",
        "duration": (3, 15),
        "res": {"720p": "768p", "1080p": "1080p"},
    },
}


def convert(sora_body: dict, target: str) -> tuple[str, dict]:
    """Translate a Sora 2 request body into (model_uuid, body) for a target."""
    t = TARGETS[target]
    ratio, res = SIZE_MAP[sora_body.get("size", "720*1280")]
    lo, hi = t["duration"]
    duration = max(lo, min(hi, int(sora_body.get("duration", 4))))

    body = {
        "prompt": sora_body["prompt"],
        "resolution": t["res"][res],
        "duration": duration,
    }
    is_i2v = "image" in sora_body
    if is_i2v:
        body["image"] = sora_body["image"]
    else:
        body["aspect_ratio"] = ratio

    if target == "seedance":
        body["generate_audio"] = True
    elif target == "wan":
        body["enable_audio"] = True

    return (t["i2v"] if is_i2v else t["t2v"], body)

convert(sora_request, "h3") on the 8-second example above returns wavespeed-ai/minimax-h3/text-to-video with aspect_ratio: "16:9", resolution: "768p", duration: 8. A 20-second Sora request becomes 15 seconds on H3 and stays 20 on the other two.

Submit and poll

Every model on WaveSpeedAI uses the same two calls: POST https://api.wavespeed.ai/api/v3/<model_uuid> with a bearer key, then GET https://api.wavespeed.ai/api/v3/predictions/<id>/result until the status is terminal. The submit response carries the prediction ID at data.id. The result response carries data.status, data.outputs (a list of URLs on success), and data.error on failure.

import os
import time
import requests

API = "https://api.wavespeed.ai/api/v3"
HEADERS = {
    "Authorization": f"Bearer {os.environ['WAVESPEED_API_KEY']}",
    "Content-Type": "application/json",
}
TERMINAL_FAILURES = {"failed", "cancelled", "timeout", "deleted"}


def generate_video(model_uuid: str, body: dict, timeout_s: int = 900) -> list[str]:
    """Submit a job and block until it completes. Returns output URLs."""
    r = requests.post(f"{API}/{model_uuid}", json=body, headers=HEADERS, timeout=60)
    r.raise_for_status()
    prediction_id = r.json()["data"]["id"]

    deadline = time.time() + timeout_s
    delay = 2.0
    while time.time() < deadline:
        time.sleep(delay)
        res = requests.get(
            f"{API}/predictions/{prediction_id}/result", headers=HEADERS, timeout=30
        )
        res.raise_for_status()
        data = res.json()["data"]
        status = data.get("status")
        if status == "completed":
            return data["outputs"]
        if status in TERMINAL_FAILURES:
            raise RuntimeError(f"{model_uuid} {status}: {data.get('error')}")
        delay = min(delay * 1.5, 10.0)

    raise TimeoutError(f"prediction {prediction_id} did not finish in {timeout_s}s")


if __name__ == "__main__":
    sora_request = {
        "prompt": "A red kite rising over a windy beach at sunset, handheld camera, waves audible",
        "size": "1280*720",
        "duration": 8,
    }
    model, body = convert(sora_request, "seedance")
    print(generate_video(model, body))

If you already called the WaveSpeedAI Sora 2 endpoints, this function is the one you have; only convert is new. If you called OpenAI’s Videos API directly, the create-then-retrieve loop maps onto submit-then-poll one to one, and the bearer header is the only auth change.

Running a fallback

Because all three targets share the request shape, a fallback is a loop over TARGETS, not a second integration:

def generate_with_fallback(sora_request: dict, order=("seedance", "h3")) -> list[str]:
    last_error = None
    for target in order:
        model, body = convert(sora_request, target)
        try:
            return generate_video(model, body)
        except (RuntimeError, TimeoutError) as e:
            last_error = e
    raise last_error

Landing pages for the three targets, with pricing and the full schema for each endpoint: Seedance 2.5 API, Wan 3.0 API, and MiniMax H3 API. For Wan-specific request examples beyond the migration case, see How do I use the Wan 3.0 API?, and for a production-grade H3 client, How to use the MiniMax H3 API.

Share