---
title: "Video"
subtitle: "Generate videos with Veo and Sora through OpenAI's Videos API."
---

Video generation is asynchronous. You create a job, poll it until it finishes, then download the content. The gateway serves OpenAI's Videos API for every video provider, so one client works for Google's Veo and Azure's Sora, with no provider key in your code.

**Pick a model:** filter by output type `video` on the [model catalog](https://www.respan.ai/models), or read [Models](/docs/ai-gateway/models/overview) for how model ids work. Resolutions are picked with [resolution variants](#resolution-variants).

## Quickstart

<Tabs>
<Tab title="cURL">
```bash
# 1. Create the job
curl https://gateway.respan.ai/v1/videos \
  -H "Authorization: Bearer $RESPAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gemini/veo-3.1-generate-preview",
    "prompt": "A paper boat drifting down a rainy street",
    "seconds": "8"
  }'

# 2. Poll until "status" is "completed" or "failed"
curl https://gateway.respan.ai/v1/videos/$VIDEO_ID \
  -H "Authorization: Bearer $RESPAN_API_KEY"

# 3. Download the video
curl https://gateway.respan.ai/v1/videos/$VIDEO_ID/content \
  -H "Authorization: Bearer $RESPAN_API_KEY" \
  -o boat.mp4
```
</Tab>
<Tab title="Python">
```python
import os
import time
from openai import OpenAI

client = OpenAI(
    base_url="https://gateway.respan.ai/v1",
    api_key=os.environ["RESPAN_API_KEY"],
)

video = client.videos.create(
    model="gemini/veo-3.1-generate-preview",
    prompt="A paper boat drifting down a rainy street",
    seconds="8",
)

while video.status in ("queued", "in_progress"):
    time.sleep(10)
    video = client.videos.retrieve(video.id)

if video.status == "completed":
    client.videos.download_content(video.id).write_to_file("boat.mp4")
else:
    print("Failed:", video.error)
```
</Tab>
<Tab title="TypeScript">
```typescript
import fs from "node:fs";
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://gateway.respan.ai/v1",
  apiKey: process.env.RESPAN_API_KEY,
});

let video = await client.videos.create({
  model: "gemini/veo-3.1-generate-preview",
  prompt: "A paper boat drifting down a rainy street",
  seconds: "8",
});

while (video.status === "queued" || video.status === "in_progress") {
  await new Promise((r) => setTimeout(r, 10_000));
  video = await client.videos.retrieve(video.id);
}

if (video.status === "completed") {
  const content = await client.videos.downloadContent(video.id);
  fs.writeFileSync("boat.mp4", Buffer.from(await content.arrayBuffer()));
}
```
</Tab>
</Tabs>

## Options

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| [`model`](#resolution-variants) | string | required | The video model, optionally with a resolution variant. |
| `prompt` | string | required | What the video shows. |
| `seconds` | string | `8` on Veo | Length of the video. |
| `size` | string | provider default | Frame size, such as `1280x720`, where the model supports it. |

## Endpoints

| Endpoint | What it does |
| --- | --- |
| `POST /v1/videos` | Creates a job and returns its `id`. |
| `GET /v1/videos/{id}` | Returns the job's `status`. |
| `GET /v1/videos/{id}/content` | Downloads the finished video. |
| `POST /v1/videos/{id}/remix` | Remixes a finished video, on providers that can. Veo can't remix, so a Veo job answers `400`. |
| `GET /v1/videos` | Not served (`400`). Keep each job's `id` and retrieve it directly. |

## Resolution variants

A catalog variant names the resolution after a colon. `gemini/veo-3.1-generate-preview:1080p` is sent to Veo as `veo-3.1-generate-preview` at 1080p, and is priced at that resolution's per-second rate.

## Jobs

When you create a job, the gateway binds it to your organization and to the provider key it was made on for 3 days. Every follow-up (poll, content, remix) goes to that same key:

- Another organization's job id returns `404`.
- If the integration the job was made on is removed, follow-ups return `424`.
- After 3 days the binding expires, so download the content before then.

## Providers

| Provider | Models | Remix |
| --- | --- | --- |
| Google Gemini | Veo | No |
| Azure OpenAI, Azure Foundry | Sora | Yes |

## Respan parameters

Send Respan parameters on the create call, in the body (or `extra_body`) or the `X-Data-Respan-Params` header. The video row written when the job ends carries the create call's `customer_identifier`, `metadata` and tracing fields. See [Respan parameters](/docs/ai-gateway/customers/respan-params).

Video requests are not redacted, so an organization with [PII redaction](/docs/ai-gateway/features/pii-redaction) on is refused.

## Pricing and logs

- A video is billed per second served, at the model's per-second rate for its resolution.
- Before a create or remix on Respan's credits is sent, the gateway holds credits for the requested seconds (8 on Veo when you don't set `seconds`).
- Creating the job writes no row. The first poll or download that sees the job end writes one `video` row and charges it.
- A failed or expired job costs $0.

## Next steps

- [Images](/docs/ai-gateway/modalities/images): generate and edit still images.
- [OSS video formats](/docs/oss-gateway/api-formats#video): how OpenAI Videos maps onto Veo.
- [Logs](/docs/ai-gateway/observability/logs-and-traces): find each video row and its cost.
- [Pricing](/docs/ai-gateway/spend/credits-vs-byok): how per-second pricing works.
