> ## Documentation Index
> Fetch the complete documentation index at: https://docs.opper.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Video

> Generate video from a text prompt or reference image with a single async endpoint.

`POST /v3/videos` is a deterministic, provider-agnostic video endpoint. You pass a
`model` and a `prompt`; Opper runs the job and gives you a URL to poll. The same
two-step flow works across every video provider (OpenAI Sora, xAI Grok-Imagine,
Pruna Wan, Google Veo) — only the contents of `parameters` differ per model.

<Info>
  Video generation is **asynchronous**: submitting returns immediately with an
  `id`, and you poll for the result. A clip typically takes from \~30 seconds to a
  few minutes depending on the model, resolution, and duration.
</Info>

## How it works

1. **Submit** — `POST /v3/videos` with `model` + `prompt` → `202 { id, status_url }`
2. **Poll** — `GET {status_url}` until `status` is `completed` or `failed`
3. **Download** — fetch the presigned `url` from the completed response (valid 1 hour)

## Run your first generation

<Steps>
  <Step title="Get an API key">
    Create a project-scoped runtime API key in the [Opper dashboard](https://platform.opper.ai).
  </Step>

  <Step title="Submit a generation">
    <Tabs>
      <Tab title="curl">
        ```bash theme={null}
        curl -sX POST https://api.opper.ai/v3/videos \
          -H "Authorization: Bearer $OPPER_API_KEY" \
          -H "Content-Type: application/json" \
          -d '{
            "model": "openai/sora-2",
            "prompt": "a hot air balloon drifting over green hills at dawn",
            "parameters": { "seconds": "4", "size": "1280x720" }
          }'
        # → 202 { "id": "gen_...", "status_url": "https://api.opper.ai/v3/artifacts/gen_.../status" }
        ```
      </Tab>

      <Tab title="Python">
        ```python theme={null}
        import os, time, requests

        BASE = "https://api.opper.ai"
        headers = {"Authorization": f"Bearer {os.environ['OPPER_API_KEY']}"}

        r = requests.post(f"{BASE}/v3/videos", headers=headers, json={
            "model": "openai/sora-2",
            "prompt": "a hot air balloon drifting over green hills at dawn",
            "parameters": {"seconds": "4", "size": "1280x720"},
        })
        r.raise_for_status()
        job = r.json()
        print("submitted:", job["id"])
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Poll until ready, then download">
    <Tabs>
      <Tab title="curl">
        ```bash theme={null}
        # Poll (repeat until status is "completed" or "failed")
        curl -s https://api.opper.ai/v3/artifacts/gen_XXX/status \
          -H "Authorization: Bearer $OPPER_API_KEY"
        # processing → { "status": "processing", "provider": "openai", "model": "openai/sora-2" }
        # done       → { "status": "completed", "mime_type": "video/mp4", "url": "https://...presigned..." }

        # Download the finished clip
        curl -s "<url-from-completed-response>" -o video.mp4
        ```
      </Tab>

      <Tab title="Python">
        ```python theme={null}
        status_url = job["status_url"]
        while True:
            s = requests.get(status_url, headers=headers).json()
            if s["status"] == "completed":
                data = requests.get(s["url"]).content   # presigned; no auth header needed
                open("video.mp4", "wb").write(data)
                print("saved video.mp4", len(data), "bytes")
                break
            if s["status"] == "failed":
                raise SystemExit(f"generation failed: {s.get('error')}")
            time.sleep(10)
        ```
      </Tab>
    </Tabs>
  </Step>
</Steps>

## Models and parameters

`model` and `prompt` are the only fields Opper owns. Everything inside
`parameters` is **forwarded verbatim** to the provider, which validates and
rejects it — so the accepted keys depend on the model. Common examples:

| Model                                | Example `parameters`                                                     |
| ------------------------------------ | ------------------------------------------------------------------------ |
| `openai/sora-2`                      | `{ "seconds": "4", "size": "1280x720" }`                                 |
| `xai/grok-imagine-video`             | `{ "duration": 6, "aspect_ratio": "16:9", "resolution": "720p" }`        |
| `pruna/wan-t2v`                      | `{ "resolution": "720p", "aspect_ratio": "16:9" }`                       |
| `vertexai/veo-3.1-fast-generate-001` | `{ "durationSeconds": 4, "resolution": "720p", "generateAudio": false }` |

To discover what's available programmatically — including each model's accepted
`aspect_ratios`, `resolutions`, and `max_duration` — call
[`GET /v3/videos/models`](/v3-api-reference/video/list-video-models):

```bash theme={null}
curl -s "https://api.opper.ai/v3/videos/models" \
  -H "Authorization: Bearer $OPPER_API_KEY"
```

Each item's `capabilities` tells you the input modality: `video_generation` is
text-to-video, `image_to_video` needs a reference image, and `video_editing` is
video-to-video.

## Reference image and video

Alongside `prompt`, you can pass a top-level `image` (image-to-video) or `video`
(video-to-video). Each is an http(s) URL, a data-URI, or a `file_id`.

* **`image`** — supported by `image_to_video` models: `openai/sora-2`,
  `openai/sora-2-pro`, `vertexai/veo-3.1-generate-001`,
  `vertexai/veo-3.1-fast-generate-001`, `pruna/wan-i2v`, `xai/grok-imagine-video`.
* **`video`** — supported by `video_editing` models: `pruna/vace`.

<Note>
  A `file_id` from a previous [image generation](/build/multimodal/images) or upload works as the seed `image` here — no re-upload. See [Files](/build/multimodal/files) for how files work, lifecycle, and storage quotas.
</Note>

```bash Seed a video from a file_id theme={null}
curl -sX POST https://api.opper.ai/v3/videos \
  -H "Authorization: Bearer $OPPER_API_KEY" -H "Content-Type: application/json" \
  -d '{ "model": "openai/sora-2", "prompt": "the cat blinks slowly", "image": "file_abc123" }'
```

```bash theme={null}
# image-to-video (Sora): image URL or base64 data-URI
curl -sX POST https://api.opper.ai/v3/videos \
  -H "Authorization: Bearer $OPPER_API_KEY" -H "Content-Type: application/json" \
  -d '{
    "model": "openai/sora-2",
    "prompt": "the cat slowly turns its head and blinks",
    "image": "https://example.com/cat.jpg",
    "parameters": { "seconds": "4", "size": "720x1280" }
  }'
```

Source-format rules differ per provider:

| Provider   | `image` / `video` accepts                                                                                                                                                            |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Sora       | http URL **or** base64/data-URI. The image's pixel dimensions must equal the requested `size`.                                                                                       |
| Veo        | base64 / data-URI / `gs://` URI only — an http URL is not fetched. (A `gs://` object must be readable by Opper's Vertex service account, so base64/data-URI is the reliable choice.) |
| Pruna, xAI | a **public, server-fetchable** URL only (a data-URI or private URL won't work).                                                                                                      |

## Reference

* [Generate a video](/v3-api-reference/video/create-video) — `POST /v3/videos`
* [List video models](/v3-api-reference/video/list-video-models) — `GET /v3/videos/models`
