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

Video generation

Video generation

Generate short video clips from a text prompt or images with an asynchronous API

The Video API runs video models that turn a text prompt, optionally with images, 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.

Model discovery

Browse inworld.ai/models and select Video for model IDs and pricing. Before submitting a job, check the selected model's supported parameters and, if you use images, its image-input support. The tables describe the models shown; they are not an exhaustive catalog.

To list them in code, call the List models API with output_modalities=video. It returns the video models you can call on /v1/videos:

bash
curl -s "https://api.inworld.ai/llm/v1alpha/models?output_modalities=video" \
  -H "Authorization: Basic $INWORLD_API_KEY"

How it works

Video generation runs as an asynchronous job:

  1. Create a job with a model, prompt and supported parameters.
  2. Save the job ID and poll until it reaches a terminal status.
  3. If the job completed, download the MP4 from output.videos[0].url.
  4. If the job failed, inspect its error before retrying.
PropertyDetails
Endpointhttps://api.inworld.ai/v1/videos
InputA text prompt, plus optional length, resolution and aspect ratio. Optionally a first and last frame (Image-to-video), or reference images, videos and audio (Reference-to-video)
OutputOne MP4 video with audio, 1 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. Some models also charge for inputs such as frame images or reference videos. Failed and cancelled jobs are not billed

Generate your first video

This example uses alibaba/wan-3.0 with a five-second, 720p 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: while the original job is retained and has not been deleted, you get it back without starting or billing a second job. 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 while it is retained and has not been deleted. After deletion or automatic removal, rerunning the script with that key starts a new paid job. Save the job's id and final result in your own database before deleting it or restarting an old request. 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):
            # Reuse the key; deletion or automatic removal ends retry protection.
            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

Request parameters

Only model and prompt are required. Omitted parameters use the model's defaults, and the returned job shows the values used. Values outside the model's range return 400 before a job is created.

ParameterPurpose
modelAPI model ID selected from the catalog
promptDescription of the video to generate
secondsRequested video length
resolutionOutput resolution supported by the selected model
aspect_ratioOutput shape supported by the selected model
frame_imagesFirst and optional last frame; see Image-to-video
input_referencesReference images, videos and audio clips; see Reference-to-video

Supported values and defaults depend on the model. See Model compatibility for the documented models.

seconds is a whole number. resolution is case-insensitive. Besides these fields, the body accepts frame_images and input_references; any other field is rejected, so a parameter is never silently ignored.

To start from a first frame or end on a last frame, see Image-to-video. To guide the video with reference images, videos or audio, see Reference-to-video. Extending or remixing an existing video is 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. Completed jobs whose video has expired are not listed. List entries do not carry download links, so retrieve each completed job once to get its link.

The list is not permanent history. Save job ids and final results in your own database if you need them after expiry or removal.

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 can be downloaded for 7 days. At the job's expires_at, the video is no longer downloadable and the job is no longer listed. Until the job is removed, retrieving it still returns completed, without output. Copy videos you want to keep to your own storage before expiry.
  • Finished jobs are removed automatically. A failed or cancelled job becomes eligible for removal 7 days after it ends. A completed job becomes eligible 7 days after its expired video is deleted. A completed job whose usage is still being recorded for billing is kept longer. Cleanup runs periodically and can take longer; do not rely on an exact removal time. Once the job is removed, retrieving it returns 404.
  • 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:

  • While the original job is retained and has not been deleted, a create with its key returns it with 200 and starts nothing new. Video expiry alone does not release the key.
  • 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.
  • Deleting a job releases its key immediately. Automatic job removal also releases the key (see Download and storage). Sending a create with a released key starts a new paid job.

Store the key, job id and final result somewhere that survives a restart, such as your app's database row for the video. Check that record before replaying an old create request: an idempotency key does not prevent duplicates after its job is deleted or automatically removed. Generating a new key on every run also defeats retry protection.

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 body128 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 finished video, at a rate that depends on the model and the resolution you choose. Some models also charge for inputs you send, such as frame images or reference videos. Current rates are on the models page.

  • 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.
  • Reference videos are billed per second of their length, rounded up to a whole second. Image and audio references aren't billed separately.
  • 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 or automatically removed
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_reusedA concurrent create could not be replayed. Check the original job before starting another with a new 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 and the same Idempotency-Key. Retry protection ends after deletion or automatic job removal
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.

Model compatibility

The following table covers the documented models. Discover model IDs and current pricing in the catalog; use this table to select compatible request parameters.

Supported parameters by model
ParameterWan 3.0 (alibaba/wan-3.0)MiniMax H3 (minimax/minimax-h3)MiniMax H3 Fast (inworld/minimax-h3-fast)MiniMax H3 Turbo (inworld/minimax-h3-turbo)Grok Imagine Video 1.5 Lite (xai/grok-imagine-video-1.5-lite)
seconds2 to 30, default 55 to 15, default 65 to 10, default 65 to 10, default 61 to 15, default 6
resolution480p (default), 720p, 1080p768p (default), 2k768p768p480p (default), 720p, 1080p
aspect_ratio16:9 (default), 4:3, 1:1, 3:4, 9:1616:9 (default), 21:9, 4:3, 1:1, 3:4, 9:1616:9 (default), 21:9, 4:3, 1:1, 3:4, 9:1616:9 (default), 21:9, 4:3, 1:1, 3:4, 9:1616:9 (default), 3:2, 4:3, 1:1, 3:4, 2:3, 9:16
prompt lengthUp to 2,000 charactersUp to 7,000 characters3 to 7,000 characters3 to 7,000 charactersUp to 4,000 characters
AudioGenerated with the videoGenerated with the videoGenerated with the videoGenerated with the videoGenerated with the video