Realtime TTS-2 is live. Built for realtime conversation that feels human. Read the Realtime TTS-2 announcement

Specialized models

Video generation

Generate short video clips from a text prompt with Alibaba Wan 3.0 and MiniMax H3

The Video API runs video models that turn a text prompt into a short MP4 clip with sound. Generation takes minutes, so it runs as a job: you create the job, poll its status until it finishes, then download the video.

PropertyDetails
Endpointhttps://api.inworld.ai/v1/videos
Modelsalibaba/wan-3.0, minimax/minimax-h3
InputA text prompt, plus optional length, resolution and aspect ratio
OutputOne MP4 video with audio, 2 to 30 seconds long, up to 1080p or 2K depending on the model
DeliveryAsynchronous: create a job, poll it, download the result
BillingPer second of finished video. Failed and cancelled jobs are not billed

Generate your first video

You need:

  • An Inworld API key. Copy its Base64 credentials from Portal or the CLI; see authentication.
  • A payment method on your account.
  • curl and a shell. Run these commands on your server, not in a browser.

Set your API key and a request key

bash
export INWORLD_API_KEY="<your Base64 API key>"
export VIDEO_REQUEST_KEY="$(uuidgen)"

VIDEO_REQUEST_KEY is the idempotency key for this one video. If a create request times out, send it again with the same key: you get the original job back, and no second job is started or billed. Generate a new key for each new video. If uuidgen isn't installed, any unique string of up to 255 characters works.

Create a job

bash
curl -X POST https://api.inworld.ai/v1/videos \
  -H "Authorization: Basic $INWORLD_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $VIDEO_REQUEST_KEY" \
  -d '{
    "model": "alibaba/wan-3.0",
    "prompt": "A red fox trots across a snowy meadow at sunrise, slow tracking shot, soft golden light",
    "seconds": 5,
    "resolution": "720p",
    "aspect_ratio": "16:9"
  }'

The API returns the job ID without waiting for generation:

json
{
  "id": "video_8f14e45fceea167a5a36dedd4bea2543",
  "object": "video",
  "model": "alibaba/wan-3.0",
  "status": "queued",
  "created_at": 1790000000,
  "seconds": 5,
  "resolution": "720p",
  "aspect_ratio": "16:9",
  "operation": "workspaces/my-workspace/videoGenerationJobs/video_8f14e45fceea167a5a36dedd4bea2543/operations/1790000000000-a1b2c3"
}

Save the id from your response. Replace the example value with yours:

bash
export VIDEO_ID="video_8f14e45fceea167a5a36dedd4bea2543"

Poll until the job finishes

bash
curl "https://api.inworld.ai/v1/videos/$VIDEO_ID" \
  -H "Authorization: Basic $INWORLD_API_KEY"

Repeat every 10 to 20 seconds until status is completed, failed or cancelled. A completed job includes a download link in output.videos[0].url:

json
{
  "id": "video_8f14e45fceea167a5a36dedd4bea2543",
  "object": "video",
  "model": "alibaba/wan-3.0",
  "status": "completed",
  "created_at": 1790000000,
  "completed_at": 1790000142,
  "expires_at": 1790604942,
  "seconds": 5,
  "resolution": "720p",
  "aspect_ratio": "16:9",
  "output": {
    "videos": [
      {
        "index": 0,
        "content_type": "video/mp4",
        "bytes": 4823311,
        "url": "https://storage.googleapis.com/...",
        "url_expires_at": 1790003742
      }
    ]
  }
}

If the job failed, its error.code says why; see job failures.

Download the video

Copy output.videos[0].url from your response. The link is a signed HTTPS URL, so download it without the Authorization header:

bash
export VIDEO_URL="<output.videos[0].url from your response>"
curl -o fox.mp4 "$VIDEO_URL"

fox.mp4 is saved in the current directory. The link stops working at url_expires_at; retrieve the job again for a new one.

Generate a video with a script

These scripts run the same create, poll and download flow. They read INWORLD_API_KEY and VIDEO_REQUEST_KEY from the environment, so set both as in step 1.

Because the key comes from the environment, rerunning a script after a timeout or crash returns the same job instead of creating another, and the script picks up where it left off. To generate a new video, set a new VIDEO_REQUEST_KEY first. A script that generates its own key each run starts a new paid job every time it is restarted.

Requires Python 3.9 or later. Install the requests package:

bash
python -m pip install requests

Save this as generate_video.py:

generate_video.py
import os
import time

import requests

API = "https://api.inworld.ai/v1/videos"
HEADERS = {"Authorization": f"Basic {os.environ['INWORLD_API_KEY']}"}
# Reuse one key for every attempt at this video, including after a restart.
REQUEST_KEY = os.environ["VIDEO_REQUEST_KEY"]


def create_video():
    for attempt in range(3):
        try:
            response = requests.post(
                API,
                headers={**HEADERS, "Idempotency-Key": REQUEST_KEY},
                json={
                    "model": "alibaba/wan-3.0",
                    "prompt": "A red fox trots across a snowy meadow at sunrise, slow tracking shot, soft golden light",
                    "seconds": 5,
                    "resolution": "720p",
                    "aspect_ratio": "16:9",
                },
                timeout=30,
            )
            response.raise_for_status()
            return response.json()
        except (requests.ConnectionError, requests.Timeout):
            # Safe to resend: the same key returns the same job.
            if attempt == 2:
                raise
            time.sleep(2**attempt)


video_id = create_video()["id"]
print("Job", video_id)

# Retrieve before reading the result: only a retrieve returns the download link.
while True:
    response = requests.get(f"{API}/{video_id}", headers=HEADERS, timeout=30)
    response.raise_for_status()
    video = response.json()
    print(video["status"])
    if video["status"] not in ("queued", "in_progress"):
        break
    time.sleep(15)

if video["status"] != "completed":
    raise SystemExit(f"Job ended {video['status']}: {video.get('error')}")

# The signed link needs no Authorization header.
url = video["output"]["videos"][0]["url"]
with requests.get(url, stream=True, timeout=300) as download:
    download.raise_for_status()
    with open("fox.mp4", "wb") as f:
        for chunk in download.iter_content(chunk_size=1 << 20):
            f.write(chunk)
print("Saved fox.mp4")

Run it:

bash
python generate_video.py

Expected output, with your job's id:

text
Job video_8f14e45fceea167a5a36dedd4bea2543
queued
in_progress
in_progress
completed
Saved fox.mp4

Models and parameters

Every parameter except model and prompt is optional. A parameter you leave out takes the model's default, and the job you get back shows the value used. Values outside a model's range are rejected with 400 before a job is created.

ParameterWan 3.0 (alibaba/wan-3.0)MiniMax H3 (minimax/minimax-h3)
seconds2 to 30, default 55 to 15, default 6
resolution480p (default), 720p, 1080p768p (default), 2k
aspect_ratio16:9 (default), 4:3, 1:1, 3:4, 9:1616:9 (default), 21:9, 4:3, 1:1, 3:4, 9:16
prompt lengthUp to 2,000 charactersUp to 7,000 characters
AudioGenerated with the videoGenerated with the video

seconds is a whole number. resolution is case-insensitive. The request body accepts only these five fields; any other field is rejected, so a parameter is never silently ignored.

The API generates video from text only. Image-to-video, reference images, and extending or remixing an existing video are not supported.

Job lifecycle

statusMeaningTerminal
queuedAccepted; waiting for or being submitted for generation. Cancellation may already be unavailableNo
in_progressThe model is generating the videoNo
completedGeneration succeeded; download available until expires_atYes
failedGeneration did not succeed; error says whyYes
cancelledCancelled before generation startedYes

A job that takes longer than 30 minutes to generate fails with generation_timeout.

Poll for the result

Retrieve the job every 10 to 20 seconds until its status is terminal. Polling more often does not make a job finish sooner. The job object has no progress percentage, so show a spinner or elapsed time rather than a progress bar.

When you track many jobs, list them instead of retrieving each one: a single list call returns up to 100 jobs with their statuses, newest first. List entries do not carry download links, so retrieve each completed job once to get its link.

Poll the operation instead

Each job is also a long-running operation, named in the job's operation field. If your app already polls Inworld operations, you can poll a video job the same way. Save the operation from your create response, replacing the example value with yours:

bash
export OPERATION="workspaces/my-workspace/videoGenerationJobs/video_8f14e45fceea167a5a36dedd4bea2543/operations/1790000000000-a1b2c3"
curl "https://api.inworld.ai/lro/v1alpha/$OPERATION" \
  -H "Authorization: Basic $INWORLD_API_KEY"

While the job is unfinished, the operation has done: false and metadata.state is QUEUED or IN_PROGRESS. When the job finishes, done becomes true and the operation carries one of:

  • response, for a completed job: the video's id, model, duration, resolution, aspectRatio, sizeBytes, mimeType and expireTime.
  • error, for a failed or cancelled job. For a failed job, the error's reason is the job's error.code.
json
{
  "name": "workspaces/my-workspace/videoGenerationJobs/video_8f14e45fceea167a5a36dedd4bea2543/operations/1790000000000-a1b2c3",
  "done": true,
  "response": {
    "@type": "type.googleapis.com/ai.inworld.video.v1.VideoGenerationResponse",
    "id": "video_8f14e45fceea167a5a36dedd4bea2543",
    "model": "alibaba/wan-3.0",
    "duration": "5s",
    "resolution": "720p",
    "aspectRatio": "16:9",
    "sizeBytes": "4823311",
    "mimeType": "video/mp4",
    "expireTime": "2026-09-28T14:15:42Z"
  }
}

The operation never carries the download link. When it is done, retrieve the job by its id to get the link. The operation can lag the job by a few seconds, so treat the job as the source of truth.

Download and storage

  • Download links remain valid for up to one hour, capped by the video's retention deadline. url_expires_at says when. Each retrieve returns a new link, so retrieve the job again rather than storing the link.
  • Videos are kept for 7 days. After the job's expires_at, the job still shows completed but has no output. Copy videos you want to keep to your own storage.
  • Treat a download link like a password. Anyone who has it can download the video until it expires.
  • Delete a video sooner with Delete video. Only finished jobs can be deleted.
  • Save your prompt yourself. The job object does not include the prompt, and you can't get it back later.

Retries and idempotency

A create request starts a paid job. If the request times out or the connection drops, you can't tell whether the job was created, and sending it again could start a second one. To retry safely, send an Idempotency-Key header with a unique value, such as a UUID, and reuse the same value when you retry:

  • A create with a key you already used returns the original job with 200, and starts nothing new.
  • Keys are scoped to your workspace and can be up to 255 characters.
  • The body is not compared with the original. A different request sent with a used key still returns the first job, so use a new key for every new video.
  • A create that is refused with 400 or 429 starts nothing and doesn't use up the key. Fix the request or wait, then resend it with the same key.
  • A retry that arrives while the first request with the same key is still being processed returns 409 with code: "idempotency_key_in_use". Wait a few seconds, then resend it with the same key.
  • A key whose job you deleted can't be reused: the create returns 409 with code: "idempotency_key_reused".

Keep the key somewhere that survives a restart, such as an environment variable, a file, or the database row for the video in your app. A key generated fresh on every run protects nothing: a rerun after a timeout gets a new key and starts a second paid job.

Cancel a job

Cancel video stops a job that has not started generating, and a cancelled job is not billed. Jobs are sent for generation as soon as they are created, so a cancel usually succeeds only right after the create. After that, cancelling returns 409 with code: "video_not_cancellable", even if the job still shows queued, and the job runs to the end.

Limits

LimitValue
Unfinished jobs per account2. A create while 2 jobs are queued or in_progress returns 429 with code: "concurrent_video_jobs_limit"
Create requestsRate limited per account. Excess requests return 429 with the plain-text body Rate limit exceeded
Request body64 KiB
Payment methodRequired. Without one, a create returns 400 with code: "payment_method_required"
Zero data retentionNot available. Workspaces with zero data retention get 400 with code: "zero_data_retention_unsupported"

Retrieving, listing, cancelling and deleting jobs is not limited by the job limit.

Billing

You pay per second of video, at the rate for the model and resolution you chose:

ModelResolutionPrice per second
alibaba/wan-3.0480p$0.05
alibaba/wan-3.0720p$0.10
alibaba/wan-3.01080p$0.20
minimax/minimax-h3768p$0.08
minimax/minimax-h32k$0.13

For example, a 5-second Wan 3.0 video at 720p costs $0.50.

  • Only completed jobs are billed. failed and cancelled jobs cost nothing.
  • Billed seconds are the length of the delivered video rounded up to a whole second, and never more than the seconds you requested.
  • Deleting a video does not refund it.

Errors

Video API errors use the JSON structure below, with a machine-readable code and a param naming the field when one is at fault. Authentication, permission, and some rate-limit errors may return plain text instead. Check the HTTP status and response content type before parsing the body.

json
{
  "error": {
    "message": "seconds must be an integer from 2 to 30",
    "type": "invalid_request_error",
    "code": "invalid_request",
    "param": "seconds"
  }
}
StatuscodeWhat to do
400invalid_requestFix the field named in param, then retry
400payment_method_requiredAdd a payment method to your account
400zero_data_retention_unsupportedUse a workspace without zero data retention
401, 403Plain textCheck your API key and its permissions
404video_not_foundThe id is wrong, belongs to another workspace, or the job was deleted
409video_not_cancellableGeneration has started; let the job finish
409video_not_terminalWait for the job to finish before deleting it
409idempotency_key_in_useThe first request with this Idempotency-Key is still being processed. Wait a few seconds, then resend with the same key
409idempotency_key_reusedSend the create with a new Idempotency-Key
429concurrent_video_jobs_limitWait for one of your jobs to finish, then resend with the same Idempotency-Key
429Plain text Rate limit exceededToo many create requests. Wait, then resend with exponential backoff and the same Idempotency-Key
500None, or a plain-text bodyResend with backoff. For a create, use the same Idempotency-Key so a retry can't start a second job
503service_unavailableResend later with backoff

Job failures

A job that fails after it was created returns status: "failed" and an error object on the job itself, not an HTTP error:

error.codeMeaning
content_filteredThe model provider's content policy blocked the prompt or the generated video. Revise the prompt
invalid_requestThe model provider rejected the request, for example over the prompt's content or length. Revise the prompt
generation_failedGeneration failed for another reason. Retry with a new Idempotency-Key
generation_timeoutGeneration did not finish within 30 minutes. Retry, or request a shorter or lower-resolution video

Failed jobs are not billed.

Coming from other video APIs

When migrating an existing integration, update the endpoint, request fields, status handling, and download logic to match this API. Call it over HTTP rather than through another provider's SDK. Differences that commonly need changes:

  • Set resolution and aspect_ratio, not size in pixels.
  • seconds is a number, not a string.
  • The body is JSON, not a multipart form.
  • Download from output.videos[0].url; there is no /content endpoint.
  • Jobs report no progress percentage.
  • POST /v1/videos/{video_id}/cancel cancels a job before it starts.