Video generation
Image-to-video
Start a video from a first frame, or move between a first and last frame
The Video API can start from images as well as a text prompt. Send them in frame_images in POST /v1/videos, then poll and download the job as usual. The image tagged first_frame becomes the video's first frame, and an optional image tagged last_frame becomes its last frame. Use it to animate a keyframe you already made, or to move between two keyframes.
To show the model subjects without fixing any frame, use reference images, videos or audio instead. A request uses frame_images or input_references, not both.
Choose a model
Browse inworld.ai/models and select Video. Choose a model that supports the frames you need: a first frame, or a first and last frame. These are separate capabilities; accepting a first frame does not imply support for a last frame.
See Model compatibility for the documented models' image-input support.
Each image is an image_url item, given by an HTTPS URL. The steps below reuse the INWORLD_API_KEY and VIDEO_REQUEST_KEY variables from Generate your first video. Use a new VIDEO_REQUEST_KEY for each new video. The Python examples need the requests package (python -m pip install requests). The Node.js examples use top-level await, so save them as .mjs files and run them with Node.js 20 or later.
Prepare your images
Upload each image, then replace the example URLs on this page with your own. Each URL must be HTTPS and download without your browser session or additional authentication headers, because the model provider downloads the image itself when generation starts. The API checks the URL when you create the job, but not the image behind it.
URL rules checked at creation
The API rejects the create request with 400 if an image URL breaks these rules:
- It must use HTTPS and name a public host. HTTP URLs, private-network IP addresses, and hosts such as
localhostor names ending in.internalor.localare rejected. - It can be at most 4,096 bytes long.
- It must be a link, not inline data. Base64
data:URLs are rejected. Upload the image somewhere the provider can download it, such as your own storage bucket.
Recommendations
These aren't checked when you create the job. If the provider can't download or use an image, the job fails after it was created:
- Keep signed URLs valid for at least an hour. The provider downloads the image when generation starts, not when you create the job. One hour is a recommendation, not an API limit.
- Check the URL before you send it. Confirm it opens and returns an image file. The API doesn't download or inspect the image.
The model provider downloads each image directly from the URL you send, so a signed URL, including its signature, is shared with that provider. Use short-lived, read-only URLs.
Start from a first frame
The examples on this page use alibaba/wan-3.0.
Put the image in frame_images with frame_type: "first_frame". The prompt describes what happens next:
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": "The fox lifts its head, looks toward the camera, then trots out of frame",
"seconds": 5,
"resolution": "720p",
"frame_images": [
{
"type": "image_url",
"image_url": { "url": "https://storage.example.com/keyframes/fox-start.png" },
"frame_type": "first_frame"
}
]
}'import os
import requests
response = requests.post(
"https://api.inworld.ai/v1/videos",
headers={
"Authorization": f"Basic {os.environ['INWORLD_API_KEY']}",
# Resend with the same key after a timeout: you get this job back, not a second one.
"Idempotency-Key": os.environ["VIDEO_REQUEST_KEY"],
},
json={
"model": "alibaba/wan-3.0",
"prompt": "The fox lifts its head, looks toward the camera, then trots out of frame",
"seconds": 5,
"resolution": "720p",
"frame_images": [
{
"type": "image_url",
"image_url": {
"url": "https://storage.example.com/keyframes/fox-start.png",
},
"frame_type": "first_frame",
},
],
},
timeout=30,
)
response.raise_for_status()
video = response.json()
print(video["id"], video["status"], video["aspect_ratio"])const response = await fetch('https://api.inworld.ai/v1/videos', {
method: 'POST',
headers: {
Authorization: `Basic ${process.env.INWORLD_API_KEY}`,
'Content-Type': 'application/json',
// Resend with the same key after a timeout: you get this job back, not a second one.
'Idempotency-Key': process.env.VIDEO_REQUEST_KEY,
},
body: JSON.stringify({
model: 'alibaba/wan-3.0',
prompt: 'The fox lifts its head, looks toward the camera, then trots out of frame',
seconds: 5,
resolution: '720p',
frame_images: [
{
type: 'image_url',
image_url: {
url: 'https://storage.example.com/keyframes/fox-start.png',
},
frame_type: 'first_frame',
},
],
}),
});
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
const video = await response.json();
console.log(video.id, video.status, video.aspect_ratio);The job comes back like any other, with aspect_ratio: "adaptive":
{
"id": "video_3c59dc048e8850243be8079a5c74d079",
"object": "video",
"model": "alibaba/wan-3.0",
"status": "queued",
"created_at": 1790000000,
"seconds": 5,
"resolution": "720p",
"aspect_ratio": "adaptive"
}Frame images can add to the cost of a job; see Billing.
The video takes its shape from the first frame, so leave aspect_ratio out or set it to "adaptive". Any other value is rejected with 400. seconds and resolution work as they do for text-only videos.
Each example prints the job's id, status and aspect ratio. To poll and download the job, use the scripts in Video generation with this request body, or poll as in Generate your first video. The job object does not list the images you sent.
End on a last frame
To control where the video ends, add a second item with frame_type: "last_frame". The model generates the motion between the two images:
"frame_images": [
{
"type": "image_url",
"image_url": { "url": "https://storage.example.com/keyframes/door-closed.png" },
"frame_type": "first_frame"
},
{
"type": "image_url",
"image_url": { "url": "https://storage.example.com/keyframes/door-open.png" },
"frame_type": "last_frame"
}
]- A last frame needs a first frame.
frame_imageswith only alast_frameis rejected. - Not every model accepts a last frame; see Model compatibility. A
last_framefor a model that doesn't accept one is rejected with400. - Send at most one
first_frameand onelast_frame. Their order in the array doesn't matter.
Use references instead
Reference images, videos and audio clips have their own guide: Reference-to-video.
Errors
Image fields are validated when you create the job. A rejected request returns 400 with code: "invalid_request" and a param that points to the item at fault. The message never repeats the URL.
param | Cause |
|---|---|
frame_images[0].type | The item's type isn't image_url |
frame_images[0].frame_type | frame_type is missing or not first_frame or last_frame, two items have the same type, or the model doesn't support a last frame |
frame_images | A last_frame was sent without a first_frame |
frame_images[0].image_url.url | The URL is missing, isn't HTTPS, names a private host, or is longer than 4,096 bytes |
input_references | Combined with frame_images |
aspect_ratio | Set to a value other than adaptive together with frame_images |
If the job fails after creation
A valid URL doesn't guarantee a usable image. The job can still fail after it was created, with status: "failed". To recover:
- Read
error.codeon the job. An image problem usually shows up asinvalid_request(the provider rejected the request or the image) orgeneration_failed, and an image the provider's content policy blocks ascontent_filtered. See Job failures. - Check that each URL still downloads without a browser session or extra headers, and that it returns an image file.
- Submit the corrected request with a new
Idempotency-Key. While the failed job is retained and has not been deleted, resending its key returns that job.
Model compatibility
Frame support by model
The table below describes frame support for the models shown; it is not an exhaustive catalog:
Wan 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) | |
|---|---|---|---|---|---|
| First frame | Yes | Yes | Yes | Yes | Yes |
| Last frame | Yes, with a first frame | Yes, with a first frame | Yes, with a first frame | Yes, with a first frame | No |