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"}
TRANSIENT = {429, 500, 502, 503, 504}


def generate_video(model_uuid: str, body: dict) -> list[str]:
    """Submit a job once and poll until it reaches a terminal status. Returns output URLs."""
    # Submit once. Do not retry this POST automatically: a retry after an ambiguous
    # failure (timeout, 5xx) could create a second paid task. If it fails that way,
    # check History in the dashboard before resubmitting.
    r = requests.post(f"{API}/{model_uuid}", json=body, headers=HEADERS, timeout=60)
    r.raise_for_status()
    prediction_id = r.json()["data"]["id"]

    # Transient poll errors back off and continue.
    delay = 2.0
    failures = 0
    while True:
        time.sleep(delay)
        try:
            res = requests.get(
                f"{API}/predictions/{prediction_id}/result", headers=HEADERS, timeout=30
            )
        except requests.RequestException:  # network error
            res = None
        if res is None or res.status_code in TRANSIENT:
            failures += 1
            retry_after = res.headers.get("Retry-After", "") if res is not None else ""
            time.sleep(int(retry_after) if retry_after.isdigit() else min(2 ** failures, 60))
            continue
        if res.status_code != 200:
            raise requests.HTTPError(f"prediction {prediction_id}: unexpected HTTP {res.status_code}")
        failures = 0
        data = res.json()["data"]
        status = data.get("status")
        if status == "completed":
            return data["outputs"]
        if status in TERMINAL_FAILURES:
            raise RuntimeError(f"{model_uuid} prediction {prediction_id} {status}: {data.get('error')}")
        delay = min(delay * 1.5, 10.0)


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 as e:  # the task reached a failure status; try the next target
            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.

FAQ

What replaces the Sora 2 size field on Seedance 2.5, Wan 3.0, and MiniMax H3?

Two fields: aspect_ratio and resolution. 1280*720 becomes aspect_ratio 16:9 with resolution 720p on Seedance 2.5 and Wan 3.0, and 768p on MiniMax H3. 720*1280 becomes 9:16 with the same resolution values. Image-to-video endpoints take the aspect ratio from the input image and have no aspect_ratio field, except Wan 3.0 where it is optional.

Do the three replacements accept Sora 2 durations of 4, 8, 12, 16, and 20 seconds?

Seedance 2.5 accepts any integer from 4 to 30 and Wan 3.0 any integer from 2 to 30, so all five Sora values pass through unchanged. MiniMax H3 accepts 3 to 15, so 16 and 20 must be clamped to 15 or split across a video-extend call.

Which fields control audio on each replacement?

Seedance 2.5 uses generate_audio, Wan 3.0 uses enable_audio, and both default to true. MiniMax H3 has no audio switch; it always generates a native soundtrack. Sora 2 on WaveSpeedAI also had no switch.

What does the WaveSpeedAI result endpoint return?

GET /api/v3/predictions/<id>/result returns a JSON object with a data field. data.status is one of created, processing, completed, failed, cancelled, timeout, or deleted. On completed, data.outputs is a list of URLs. On failed, data.error holds the message.

Share