Video generation
Generate videos with Ray 3.2 — text-to-video, image-to-video, multi-keyframe i2v, resolution, duration, looping, and HDR.
Generate videos from text or from anchor images with ray-3.2. This guide covers every parameter for type: "video" requests. For editing existing videos, see Video editing.
Basic request — text-to-video
Section titled “Basic request — text-to-video”A text-to-video request needs model, type, a prompt, and (typically) a resolution and duration under video:
from luma_agents import Luma
client = Luma()
generation = client.generations.create( model="ray-3.2", type="video", prompt="A slow dolly shot through a misty greenhouse at sunrise", aspect_ratio="16:9", video={ "resolution": "720p", "duration": "5s", },)import Luma from "luma-agents";
const client = new Luma();
const generation = await client.generations.create({ model: "ray-3.2", type: "video", prompt: "A slow dolly shot through a misty greenhouse at sunrise", aspect_ratio: "16:9", video: { resolution: "720p", duration: "5s", },});generation, err := client.Generations.New(ctx, lumaagents.GenerationNewParams{ Model: lumaagents.F(lumaagents.ModelRay3_2), Type: lumaagents.F(lumaagents.GenerationNewParamsTypeVideo), Prompt: lumaagents.F("A slow dolly shot through a misty greenhouse at sunrise"), AspectRatio: lumaagents.F(lumaagents.GenerationNewParamsAspectRatio16_9), Video: lumaagents.F(lumaagents.VideoOptionsParam{ Resolution: lumaagents.F(lumaagents.VideoResolution720p), Duration: lumaagents.F(lumaagents.VideoDuration5s), }),})curl -X POST https://agents.lumalabs.ai/v1/generations \ -H "Authorization: Bearer $LUMA_AGENTS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "ray-3.2", "type": "video", "prompt": "A slow dolly shot through a misty greenhouse at sunrise", "aspect_ratio": "16:9", "video": { "resolution": "720p", "duration": "5s" } }'Output is delivered as an MP4 via a presigned URL once the generation completes — same submit-poll-download pattern as image generation. Keep the completed generation’s top-level id if you want to edit, extend, or reframe it later with source.generation_id or a generation_id keyframe.
Image-to-video with anchor frames
Section titled “Image-to-video with anchor frames”Pass an image as video.start_frame, video.end_frame, or both to seed the generation. Each accepts the same ImageRef shape as image_ref on image requests — a publicly accessible url, or inline base64 data with media_type.
generation = client.generations.create( model="ray-3.2", type="video", prompt="The character turns to face the camera and smiles", aspect_ratio="16:9", video={ "resolution": "720p", "duration": "5s", "start_frame": {"url": "https://example.com/opening-frame.jpg"}, "end_frame": {"url": "https://example.com/closing-frame.jpg"}, },)const generation = await client.generations.create({ model: "ray-3.2", type: "video", prompt: "The character turns to face the camera and smiles", aspect_ratio: "16:9", video: { resolution: "720p", duration: "5s", start_frame: { url: "https://example.com/opening-frame.jpg" }, end_frame: { url: "https://example.com/closing-frame.jpg" }, },});generation, err := client.Generations.New(ctx, lumaagents.GenerationNewParams{ Model: lumaagents.F(lumaagents.ModelRay3_2), Type: lumaagents.F(lumaagents.GenerationNewParamsTypeVideo), Prompt: lumaagents.F("The character turns to face the camera and smiles"), AspectRatio: lumaagents.F(lumaagents.GenerationNewParamsAspectRatio16_9), Video: lumaagents.F(lumaagents.VideoOptionsParam{ Resolution: lumaagents.F(lumaagents.VideoResolution720p), Duration: lumaagents.F(lumaagents.VideoDuration5s), StartFrame: lumaagents.F(lumaagents.ImageRefParam{ URL: lumaagents.F("https://example.com/opening-frame.jpg"), }), EndFrame: lumaagents.F(lumaagents.ImageRefParam{ URL: lumaagents.F("https://example.com/closing-frame.jpg"), }), }),})curl -X POST https://agents.lumalabs.ai/v1/generations \ -H "Authorization: Bearer $LUMA_AGENTS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "ray-3.2", "type": "video", "prompt": "The character turns to face the camera and smiles", "aspect_ratio": "16:9", "video": { "resolution": "720p", "duration": "5s", "start_frame": {"url": "https://example.com/opening-frame.jpg"}, "end_frame": {"url": "https://example.com/closing-frame.jpg"} } }'Multi-keyframe image-to-video
Section titled “Multi-keyframe image-to-video”video.start_frame / video.end_frame only pin the first and last frame. To guide the motion through the clip — pinning images at arbitrary positions — use two parallel arrays under video:
keyframes— 1–64 guide-frame images, each taking the sameImageRefshape as the anchor frames above (aurl, inline base64data, or a prior generation’sgeneration_id).keyframe_indexes— a parallel list of non-negative, unique output-frame positions where eachkeyframes[i]is anchored, in theduration × 24fpsgrid:5s → 0–120,10s → 0–240.
The two arrays must be the same length, and you provide both or neither — one without the other is a 400. keyframes is the multi-anchor generalization of start_frame: a single anchor at index 0 (keyframes: [X], keyframe_indexes: [0]) is equivalent to start_frame: X, so the two surfaces are mutually exclusive — setting both on one request is a 400. Use one or the other.
Unlike the legacy start_frame / end_frame pair, multi-keyframe i2v routes through the full Ray 3.2 path, so it lifts those anchors’ restrictions: arbitrary frame positions, duration: "10s", and hdr: true are all supported.
generation = client.generations.create( model="ray-3.2", type="video", prompt="A hot-air balloon drifts across the valley as the sun rises", aspect_ratio="16:9", video={ "resolution": "720p", "duration": "5s", "keyframes": [ {"url": "https://example.com/launch.jpg"}, {"url": "https://example.com/midflight.jpg"}, {"url": "https://example.com/sunrise.jpg"}, ], "keyframe_indexes": [0, 60, 120], },)const generation = await client.generations.create({ model: "ray-3.2", type: "video", prompt: "A hot-air balloon drifts across the valley as the sun rises", aspect_ratio: "16:9", video: { resolution: "720p", duration: "5s", keyframes: [ { url: "https://example.com/launch.jpg" }, { url: "https://example.com/midflight.jpg" }, { url: "https://example.com/sunrise.jpg" }, ], keyframe_indexes: [0, 60, 120], },});curl -X POST https://agents.lumalabs.ai/v1/generations \ -H "Authorization: Bearer $LUMA_AGENTS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "ray-3.2", "type": "video", "prompt": "A hot-air balloon drifts across the valley as the sun rises", "aspect_ratio": "16:9", "video": { "resolution": "720p", "duration": "5s", "keyframes": [ { "url": "https://example.com/launch.jpg" }, { "url": "https://example.com/midflight.jpg" }, { "url": "https://example.com/sunrise.jpg" } ], "keyframe_indexes": [0, 60, 120] } }'Extending a prior video
Section titled “Extending a prior video”To continue or prepend a previous video, pass that completed generation’s top-level id as exactly one generation_id keyframe on a type: "video" request:
| Shape | Result |
|---|---|
video.start_frame: { "generation_id": "<id>" } | Forward extend — the prior clip becomes the start, and the new generation continues after it |
video.end_frame: { "generation_id": "<id>" } | Backward extend — the prior clip becomes the end, and the new generation is prepended before it |
{ "model": "ray-3.2", "type": "video", "prompt": "continue the camera move into the glowing forest", "video": { "resolution": "720p", "start_frame": { "generation_id": "d290f1ee-6c54-4b01-90e6-d701748f0851" } }}Parameters
Section titled “Parameters”prompt (required)
Section titled “prompt (required)”A text description of the video, 1–6,000 characters. Be specific about subject, motion, camera movement, lighting, and pacing.
Use ray-3.2 for video generation.
"video" for generation. For editing an existing video, use "video_edit" — see Video editing. For aspect-ratio reframing, use "video_reframe" — see Video reframing.
aspect_ratio
Section titled “aspect_ratio”Ray 3.2 video accepts these six aspect ratios — a subset of the API-wide AspectRatio enum:
| Value | Orientation |
|---|---|
9:16 | Standard portrait |
3:4 | Portrait |
1:1 | Square |
4:3 | Classic landscape |
16:9 | Standard widescreen |
21:9 | Cinematic ultrawide |
When omitted, the model selects a ratio based on the prompt and anchor frames.
video.resolution
Section titled “video.resolution”Output resolution. Defaults to 720p.
| Value | Notes |
|---|---|
360p | Draft tier — fast, low-cost previews. SDR only (rejected with hdr: true) |
540p | Standard definition. Not available with hdr: true |
720p | HD — default |
1080p | Full HD |
video.duration
Section titled “video.duration”Video duration. Defaults to 5s.
| Value | Notes |
|---|---|
5s | Default |
10s | Not supported with hdr: true, start_frame, or end_frame |
video.loop
Section titled “video.loop”Boolean. When true, generates a seamlessly looping video.
video.hdr
Section titled “video.hdr”Boolean. When true, generates an HDR-encoded MP4.
video.exr_export
Section titled “video.exr_export”Boolean. When true, exports an EXR file alongside the MP4 for professional colour-grading workflows. Requires hdr: true.
video.start_frame / video.end_frame
Section titled “video.start_frame / video.end_frame”Optional anchor ImageRefs. start_frame is the first frame (or, on video_edit, the single guide frame). end_frame is the last frame and is valid for type: "video" only. Each can also carry a prior generation id as { "generation_id": "..." } for the single-keyframe extend flows above. Neither is supported with duration: "10s".
{ "video": { "start_frame": { "url": "https://example.com/first.jpg" }, "end_frame": { "data": "iVBORw0KGgo...", "media_type": "image/png" } }}video.keyframes / video.keyframe_indexes
Section titled “video.keyframes / video.keyframe_indexes”Optional multi-anchor image-to-video controls — see Multi-keyframe image-to-video. keyframes is a list of 1–64 guide-frame ImageRefs; keyframe_indexes is a parallel, same-length list of unique output-frame positions (duration × 24fps: 5s → 0–120, 10s → 0–240). Mutually exclusive with start_frame, end_frame, and loop.
Validation rules
Section titled “Validation rules”| Rule | Constraint |
|---|---|
model | Must be ray-3.2 for video |
type | Must be "video" for this flow |
prompt | Required, 1–6,000 characters |
aspect_ratio | One of 9:16, 3:4, 1:1, 4:3, 16:9, 21:9, or omitted |
video.resolution | One of 360p, 540p, 720p, 1080p, or omitted (defaults 720p). 360p (draft) and 540p are rejected with hdr: true |
video.duration | One of 5s, 10s, or omitted (defaults 5s). 10s is rejected with hdr, start_frame, or end_frame |
video.loop | Boolean. type: "video" only; rejected with 10s, hdr, end_frame, or keyframes |
video.hdr | Boolean. Requires 720p/1080p; rejected with 360p/540p, 10s, or loop |
video.exr_export | Boolean. Requires hdr: true |
video.start_frame, video.end_frame | Optional ImageRef (url or base64 data, max 50 MB and 8000 px per side; or a generation_id). Rejected with duration: "10s". end_frame is type: "video" only. Mutually exclusive with keyframes |
video.keyframes | Optional list of 1–64 guide-frame ImageRefs. Provide with keyframe_indexes (same length). Mutually exclusive with start_frame, end_frame, and loop |
video.keyframe_indexes | Parallel list of non-negative, unique output-frame positions (5s → 0–120, 10s → 0–240); same length as keyframes |
video.edit | Rejected for type: "video" — edit-only |
source | Rejected for type: "video" — use source only for image_edit, video_edit, or video_reframe |
source.generation_id | Rejected for type: "video" — use video.start_frame.generation_id or video.end_frame.generation_id for extend |
See Error handling for the exact detail strings rejected values produce.
Response
Section titled “Response”A successful submit returns HTTP 201:
{ "id": "d290f1ee-6c54-4b01-90e6-d701748f0851", "type": "video", "state": "queued", "model": "ray-3.2", "created_at": "2026-05-26T12:00:00Z", "output": [], "failure_reason": null, "failure_code": null}Poll GET /v1/generations/{id} until state is completed or failed. On completed, each output entry carries a presigned url to the MP4:
{ "output": [ { "type": "video", "url": "https://storage.example.com/generations/d290f1ee/output.mp4?X-Amz-Expires=3600&..." } ]}Polling
Section titled “Polling”Video generations take longer than image generations. Tune your initial wait and deadline accordingly — a 5s/720p generation typically completes in well under two minutes, but 10s/1080p with HDR can run several times longer.
import time
deadline = time.time() + 600 # 10-minute hard timeouttime.sleep(30) # don't bother polling right away
while generation.state not in ("completed", "failed"): if time.time() > deadline: raise TimeoutError(f"Generation {generation.id} did not complete in time") generation = client.generations.get(generation.id) time.sleep(5)const deadline = Date.now() + 600_000; // 10-minute hard timeoutawait new Promise((r) => setTimeout(r, 30_000));
while (generation.state !== "completed" && generation.state !== "failed") { if (Date.now() > deadline) { throw new Error(`Generation ${generation.id} did not complete in time`); } generation = await client.generations.get(generation.id); await new Promise((r) => setTimeout(r, 5_000));}deadline := time.Now().Add(10 * time.Minute)time.Sleep(30 * time.Second)
for generation.State != lumaagents.GenerationStateCompleted && generation.State != lumaagents.GenerationStateFailed { if time.Now().After(deadline) { return fmt.Errorf("generation %s did not complete in time", generation.ID) } generation, err = client.Generations.Get(ctx, generation.ID) if err != nil { return fmt.Errorf("poll: %w", err) } time.Sleep(5 * time.Second)}DEADLINE=$(($(date +%s) + 600)) # 10-minute hard timeoutsleep 30 # don't bother polling right away
while true; do if [ "$(date +%s)" -gt "$DEADLINE" ]; then echo "Generation $ID did not complete in time" >&2 exit 1 fi RESULT=$(curl -s -H "Authorization: Bearer $LUMA_AGENTS_API_KEY" \ "https://agents.lumalabs.ai/v1/generations/$ID") STATE=$(echo "$RESULT" | jq -r '.state') [ "$STATE" = "completed" ] || [ "$STATE" = "failed" ] && break sleep 5donePricing
Section titled “Pricing”Ray 3.2 video generation is priced by duration, resolution, and dynamic range. Standard dynamic range has separate 5-second and 10-second totals; HDR and HDR + EXR are 5-second only. See Pricing — ray-3.2 per-video pricing for the full grid.
Next steps
Section titled “Next steps”- Video editing — Edit existing videos with Ray 3.2
- Video reframing — Reframe an existing video to a new aspect ratio
- Models — Capability matrix for every model
- Pricing — Per-video pricing for Ray 3.2
- Error handling — Every error code with troubleshooting steps
- API Reference — Complete endpoint specifications