Windpaint
Generation

Video

Generate 5-second clips with video.generate and wan-2.2-i2v: start frames, sizes, timing, and chaining an image into a video.

video.generate turns a still image and a prompt into a short clip. It runs on wan-2.2-i2v, an image-to-video model: you give it a start frame, describe the motion, and get back a 5-second MP4 at 480p. A clip costs 4 credits (read the current price from the catalog) and takes several minutes to render.

curl -X POST https://api.windpaint.ai/v1/generation/capabilities/video.generate \
  -H "Authorization: Bearer $WINDPAINT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "slow dolly in, steam rising from the cup",
    "inputs": {"start_frame": ["8e1f2a3b-4c5d-4e6f-9a0b-1c2d3e4f5a6b"]}
  }'

The start frame is an asset in your project: either a file you uploaded or the output of an earlier job, such as an image.

What it supports today

Modelwan-2.2-i2v
start_frameRequired, exactly one image asset (PNG, JPEG or WebP)
end_frameNot supported
resolution480p only (the default)
duration5 only (the default)
aspect_ratioAny of the ten ratios; default 16:9
OutputOne MP4 clip, video/mp4, no audio track
Price4 credits per clip

Current limits, plainly: there is no text-only video model, so every clip needs a start frame. There’s no end frame or first/last-frame interpolation, no audio, one resolution and one duration. The video.generate capability defines an end_frame slot and higher tiers (720p, 1080p) for future models, but wan-2.2-i2v rejects them with 422.

Sizes

Video tiers set the short edge, and the aspect ratio sets the long edge, rounded to a multiple of 16:

aspect_ratioOutput size
16:9 (default)848 × 480
9:16480 × 848
1:1480 × 480
4:3 / 3:4640 × 480 / 480 × 640
3:2 / 2:3720 × 480 / 480 × 720
21:91120 × 480
5:4 / 4:5608 × 480 / 480 × 608

The clip’s size comes from aspect_ratio, not from the start frame. Set aspect_ratio to match your start frame (or the closest of the ten), otherwise the clip is rendered at a different shape from the frame and may not frame it the way you expect. If you generate the start frame with image.generate, use the same aspect_ratio for both jobs.

Prompting for motion

The start frame already fixes what’s in the shot, so the prompt works best when it describes what moves: the camera (“slow dolly in”, “orbit left”, “static shot”), the subject (“she turns toward the window”), and the environment (“steam rising”, “leaves drifting”). Restating the whole scene adds little. A prompt is required.

Timing: poll or use a webhook

A clip usually takes a few minutes from submit to completed, and longer when there’s a queue. Plan for that:

  • Polling: back off to a 10–15 second interval and give the loop an overall timeout of at least 20 minutes. The CLI waits up to 15 minutes by default; raise it with --timeout 30m, or use --no-wait and come back later with windpaint status <id> --wait.
  • Webhook: pass webhook_url and Windpaint POSTs the job’s status to it when the clip finishes. Treat it as a notification and confirm with a status call; see Webhooks.

A failed, refused (nsfw) or canceled clip is not charged.

Image to video, end to end

Generate a still, then animate it. The image’s output id goes straight into the video’s start_frame; nothing needs to be downloaded or re-uploaded.

Python
import os
import time
import requests

API = "https://api.windpaint.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['WINDPAINT_API_KEY']}"}
TERMINAL = {"completed", "failed", "nsfw", "canceled"}


def submit(capability: str, body: dict) -> str:
    resp = requests.post(f"{API}/generation/capabilities/{capability}", headers=HEADERS, json=body)
    resp.raise_for_status()
    return resp.json()["request_id"]


def wait(request_id: str, timeout_s: float = 1800) -> dict:
    delay, deadline = 2.0, time.monotonic() + timeout_s
    while True:
        job = requests.get(f"{API}/generation/requests/{request_id}/status", headers=HEADERS).json()
        if job["status"] in TERMINAL:
            if job["status"] != "completed":
                raise RuntimeError(f"{request_id} {job['status']}: {job['error']}")
            return job
        if time.monotonic() > deadline:
            raise TimeoutError(f"{request_id} still {job['status']}")
        time.sleep(delay)
        delay = min(delay * 1.5, 15)


# 1. The start frame, at the same aspect ratio as the clip.
image = wait(submit("image.generate", {
    "prompt": "a paper boat on a rain-soaked street, puddles, evening light",
    "aspect_ratio": "16:9",
    "seed": 11,
}))
frame_id = image["outputs"][0]["id"]

# 2. The clip.
clip = wait(submit("video.generate", {
    "prompt": "slow push in, rain falling, ripples spreading around the boat",
    "inputs": {"start_frame": [frame_id]},
    "aspect_ratio": "16:9",
}))

video = clip["video"]
with open("boat.mp4", "wb") as f:
    f.write(requests.get(video["url"], headers=HEADERS).content)
print("saved boat.mp4", video["width"], "x", video["height"], video["duration_s"], "s")

The completed status has the clip in both outputs[0] and the video shortcut, with content_type: "video/mp4", width, height and duration_s.

Use your own start frame

Upload the image first, then pass the returned asset id:

curl
curl -X POST https://api.windpaint.ai/v1/generation/uploads \
  -H "Authorization: Bearer $WINDPAINT_API_KEY" \
  -F "[email protected];type=image/jpeg"
# -> {"data": {"id": "8e1f2a3b-...", "kind": "image", ...}}

Uploads are free and must be in the same project as the video job. The frame must be an image asset; passing a video or audio asset returns 422. See Assets for limits.

Errors you’re likely to hit

ResponseCause
422 video.generate on wan-2.2-i2v: slot 'start_frame' takes 1 asset(s); got 0.No start frame.
422 ... slot 'end_frame' takes 0 asset(s); got 1.end_frame isn’t supported.
422 wan-2.2-i2v resolution must be one of 480p.Any tier other than 480p.
422 wan-2.2-i2v duration must be one of 5 seconds.Any duration other than 5.
422 Input asset not found in this project.The start frame belongs to another project, or the id is wrong.
422 Inputs must be asset ids or URLs returned by /v1/generation.You passed an external image URL. Upload the file first.
402 billing.insufficient_creditsYour balance can’t cover the 4-credit hold.

To check a video request before paying for it, POST /v1/generation/estimate with "capability": "video.generate" and the same body returns the price and size. It validates the slot counts, so include a start_frame.