---
title: Video Generation
description: How to generate videos with Sume models via the asynchronous /v1/videos API.
---

Sume supports video generation from text prompts (and optional reference
images) through a dedicated asynchronous API. To find the supported models and
their capabilities, use `GET /v1/videos/models`. The
[API rate card](https://www.sume.com/pricing/api) lists the current credits.

This surface agrees field-for-field with the [OpenRouter Video Generation
API](https://openrouter.ai/docs/guides/overview/multimodal/video-generation).
Thus, a client that you wrote from their docs works here after you change the
base URL and the API key. The few differences of Sume are in
[Sume differences](#sume-differences).

## Model Discovery

You can find video generation models in these ways:

### Via the Video Models API

To get a list of all available video generation models and their supported
parameters, use the dedicated video models endpoint:

```bash
curl "https://api.sume.com/v1/videos/models" \
  -H "Authorization: Bearer $SUME_API_KEY"
```

The response returns a `data` array. Each model in the array includes these
fields:

```json
{
  "data": [
    {
      "id": "seedance-2",
      "canonical_slug": "seedance-2",
      "name": "Seedance 2.0",
      "description": "Seedance 2.0 — text/image/reference-to-video with optional audio.",
      "created": 1767225600,
      "supported_resolutions": ["480p", "720p", "1080p"],
      "supported_aspect_ratios": ["21:9", "16:9", "4:3", "1:1", "3:4", "9:16"],
      "supported_sizes": null,
      "supported_durations": [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
      "supported_frame_images": ["first_frame", "last_frame"],
      "supported_input_references": ["image_url", "video_url", "audio_url"],
      "generate_audio": true,
      "seed": false,
      "allowed_passthrough_parameters": [],
      "hugging_face_id": null
    }
  ]
}
```

| Field                            | Description                                                                  |
| -------------------------------- | ---------------------------------------------------------------------------- |
| `id`                             | Model slug to use in generation requests                                     |
| `canonical_slug`                 | Permanent model identifier                                                   |
| `supported_resolutions`          | List of supported output resolutions (for example, `720p`, `1080p`)          |
| `supported_aspect_ratios`        | List of supported aspect ratios (for example, `16:9`, `9:16`)                |
| `supported_sizes`                | List of supported pixel dimensions (for example, `1280x720`), or `null`      |
| `supported_durations`            | Supported video lengths in whole seconds                                     |
| `supported_frame_images`         | The `frame_type` values that the model accepts                               |
| `supported_input_references`     | The `input_references` types that the model accepts                          |
| `generate_audio`                 | Shows if the model can generate an audio track                               |
| `seed`                           | Shows if the model accepts a `seed`                                          |
| `allowed_passthrough_parameters` | Provider-specific parameters that you can send through the `provider` option |

Before you submit a generation request, use this endpoint to find the
resolutions, aspect ratios, and durations that each model supports. Limits are
different for each model. `seedance-2.5` accepts 4–30 seconds at
480p/720p/1080p. `wan-3.0` accepts 2–30 seconds. `higgsfield-genjutsu` (Motion
Transfer: one source video plus 1–8 reference images, 480p/720p, in the catalog
only when its provider is configured) accepts 4–30 seconds.

`minimax-h3` accepts 5–15 seconds at native 480p/768p (768p is first-class, not 720p). For this model,
2K/4K upscales use more credits if your request includes them. Each other
catalog model has a maximum of 15 seconds. `seedance-2` also has 1080p.

Vidu v4 (`vidu-q4`) animates one `first_frame` in `frame_images` (optional
prompt, audio always on). Vidu v4 Reference (`vidu-q4-reference`) takes 1–12
image and up to 3 mp3 audio `input_references` with a required prompt. Both
accept 3–16 seconds at 540p/720p/1080p/2K/4K.

MiniMax H3 Max (`minimax-h3-max`) is the faster 768p variant. It does
text-to-video, first/last-frame image-to-video, and reference-to-video at
480p/768p/1080p (1080p is a latent refinement from native 768p) for 5–15
seconds, with native stereo audio. It reports image, video, and audio
`supported_input_references`. Gemini Omni Flash 1.1 (`gemini-omni-flash-1.1`)
accepts 3–10 seconds at 360p/720p/1080p/4K in 16:9 or 9:16, with native synced
audio. It accepts image and video `input_references` (no audio). It makes its
video edit mode available through the Video Router `video_url` field.

### Via the Models API

You can also use the [public model catalog](/models) to find video generation
models:

```bash
curl "https://api.sume.com/v1/catalog" \
  -H "Authorization: Bearer $SUME_API_KEY"
```

### On the Models Page

Go to the [Models page](/models). Find the models that show `video` as an
output modality.

## How It Works

Different from text or image generation, video generation is **asynchronous**,
because a video takes much more time to generate. The workflow has these steps:

1. **Submit** a generation request to `POST /v1/videos`
2. **Receive** a job ID and a polling URL immediately
3. **Poll** the polling URL (`GET /v1/videos/{jobId}`) until the status is `completed`
4. **Download** the video from the content URL (`GET /v1/videos/{jobId}/content`)

## API Usage

### Submitting a Video Generation Request

```python title="Python"
import requests
import time

url = "https://api.sume.com/v1/videos"
headers = {
    "Authorization": f"Bearer {SUME_API_KEY}",
    "Content-Type": "application/json",
}

payload = {
    "model": "seedance-2",
    "prompt": "A golden retriever playing fetch on a sunny beach with waves crashing in the background",
}

# Step 1: Submit the generation request
response = requests.post(url, headers=headers, json=payload)
result = response.json()

job_id = result["id"]
polling_url = result["polling_url"]
print(f"Job submitted: {job_id}")
print(f"Status: {result['status']}")

# Step 2: Poll until completion
while True:
    time.sleep(30)  # Wait 30 seconds between polls
    poll_response = requests.get(polling_url, headers=headers)
    status = poll_response.json()

    print(f"Status: {status['status']}")

    if status["status"] == "completed":
        # Step 3: Download the video
        content_url = status["unsigned_urls"][0]
        video_response = requests.get(content_url)
        with open("output.mp4", "wb") as f:
            f.write(video_response.content)
        print("Video saved to output.mp4")
        break
    elif status["status"] == "failed":
        print(f"Generation failed: {status.get('error', 'Unknown error')}")
        break
```

```typescript title="TypeScript (fetch)"
const headers = {
  Authorization: `Bearer ${SUME_API_KEY}`,
  'Content-Type': 'application/json',
};

// Step 1: Submit the generation request
const response = await fetch('https://api.sume.com/v1/videos', {
  method: 'POST',
  headers,
  body: JSON.stringify({
    model: 'seedance-2',
    prompt:
      'A golden retriever playing fetch on a sunny beach with waves crashing in the background',
  }),
});

const result = await response.json();
const jobId = result.id;
const pollingUrl = result.polling_url;
console.log(`Job submitted: ${jobId}`);
console.log(`Status: ${result.status}`);

// Step 2: Poll until completion
while (true) {
  await new Promise((resolve) => setTimeout(resolve, 30000)); // Wait 30 seconds
  const pollResponse = await fetch(pollingUrl, { headers });
  const status = await pollResponse.json();

  console.log(`Status: ${status.status}`);

  if (status.status === 'completed') {
    // Step 3: Download the video
    const contentUrl = status.unsigned_urls[0];
    console.log(`Video ready: ${contentUrl}`);
    break;
  } else if (status.status === 'failed') {
    console.error(`Generation failed: ${status.error ?? 'Unknown error'}`);
    break;
  }
}
```

```bash title="cURL"
# Step 1: Submit the generation request
curl -X POST "https://api.sume.com/v1/videos" \
  -H "Authorization: Bearer $SUME_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "seedance-2",
    "prompt": "A golden retriever playing fetch on a sunny beach with waves crashing in the background"
  }'

# Response:
# {
#   "id": "<job_id>",
#   "polling_url": "https://api.sume.com/v1/videos/<job_id>",
#   "status": "pending",
#   "model": "seedance-2"
# }

# Step 2: Poll for status
curl "https://api.sume.com/v1/videos/<job_id>" \
  -H "Authorization: Bearer $SUME_API_KEY"

# Step 3: Once status is "completed", download from unsigned_urls[0]
```

### Request Parameters

| Parameter          | Type    | Required | Description                                                                                                                   |
| ------------------ | ------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `model`            | string  | Yes      | The model for video generation (for example, `seedance-2.5`), or `sume/auto` to let Sume select the model                     |
| `prompt`           | string  | Yes      | Text description of the video to generate                                                                                     |
| `duration`         | integer | No       | Duration of the generated video in seconds                                                                                    |
| `resolution`       | string  | No       | Resolution of the output video (for example, `720p`, `1080p`)                                                                 |
| `aspect_ratio`     | string  | No       | Aspect ratio of the output video (for example, `16:9`, `9:16`, `3:2`)                                                         |
| `size`             | string  | No       | Accurate pixel dimensions in `WIDTHxHEIGHT` format (for example, `1280x720`). An alternative to `resolution` + `aspect_ratio` |
| `frame_images`     | array   | No       | Images for first/last frames (image-to-video)                                                                                 |
| `input_references` | array   | No       | Reference images for the style (reference-to-video)                                                                           |
| `generate_audio`   | boolean | No       | Tells the model to generate audio with the video, or not. The default is the audio capability of the model                    |
| `seed`             | integer | No       | Seed for deterministic generation (the result is not deterministic on all providers)                                          |
| `callback_url`     | string  | No       | URL that receives a webhook notification when the job completes. It must be HTTPS                                             |
| `provider`         | object  | No       | Provider-specific passthrough configuration                                                                                   |

### Supported Resolutions

- `480p`
- `720p`
- `768p`
- `1080p`
- `1K`
- `2K`
- `4K`

Each model shows the subset that it accepts in `supported_resolutions`.

### Supported Aspect Ratios

- `16:9` — Widescreen landscape
- `9:16` — Vertical/portrait
- `1:1` — Square
- `4:3` — Standard landscape
- `3:4` — Standard portrait
- `3:2` — Photography landscape
- `2:3` — Photography portrait
- `21:9` — Ultra-wide
- `9:21` — Ultra-tall

Each model shows the subset that it accepts in `supported_aspect_ratios`.

### Using Images

You can send images in two ways. Each way starts a different generation mode:

- **`frame_images`** — Gives first or last frame images for
  **image-to-video** generation. Each entry must include a `frame_type` of
  `first_frame` or `last_frame`.
- **`input_references`** — Gives style or content reference images for
  **reference-to-video** generation. The model uses these images as visual
  guidance, not as accurate frames.

If you send the two fields, `frame_images` controls the mode, and Sume
processes the request as image-to-video.

#### Image-to-Video (frame_images)

```json
{
  "model": "seedance-2",
  "prompt": "A character walking through a forest",
  "frame_images": [
    {
      "type": "image_url",
      "image_url": {
        "url": "https://example.com/first-frame.png"
      },
      "frame_type": "first_frame"
    }
  ],
  "resolution": "1080p"
}
```

#### Reference-to-Video (input_references)

```json
{
  "model": "seedance-2",
  "prompt": "A colossal solar flare beside a planet",
  "input_references": [
    {
      "type": "image_url",
      "image_url": {
        "url": "https://example.com/style-ref.png"
      }
    }
  ],
  "resolution": "1080p"
}
```

A model accepts a reference type only if its `supported_input_references`
includes that type. The Seedance 2.x models, Wan 3.0, MiniMax H3, and MiniMax H3
Max accept audio and video references. Gemini Omni Flash 1.1 and
`higgsfield-genjutsu` accept video references but not audio. `vidu-q4-reference` accepts image and audio references but not video.

### Provider-Specific Options

You can send provider-specific options in the `provider` parameter. Each
provider slug is the key for its options. Sume forwards only the options for the
matched provider:

```json
{
  "model": "seedance-2",
  "prompt": "A time-lapse of a flower blooming",
  "provider": {
    "options": {}
  }
}
```

To find the passthrough parameters that each model supports, read the
`allowed_passthrough_parameters` field in the
[Video Models API](#via-the-video-models-api). In v1, that list is empty for
each model. Thus, Sume rejects `provider.options` entries with an error, and
does not ignore them. Refer to [Sume differences](#sume-differences).

## Response Format

### Submit Response (202 Accepted)

When you submit a video generation request, you immediately receive a response
with the job details:

```json
{
  "id": "job_01HXYZ",
  "polling_url": "https://api.sume.com/v1/videos/job_01HXYZ",
  "status": "pending",
  "model": "seedance-2"
}
```

### Poll Response

When you poll the job status, the response includes more fields as the job
continues:

```json
{
  "id": "job_01HXYZ",
  "generation_id": "job_01HXYZ",
  "polling_url": "https://api.sume.com/v1/videos/job_01HXYZ",
  "status": "completed",
  "model": "seedance-2",
  "unsigned_urls": ["https://api.sume.com/v1/videos/job_01HXYZ/content?index=0"],
  "usage": {
    "is_byok": false
  }
}
```

### Job Statuses

| Status        | Description                                       |
| ------------- | ------------------------------------------------- |
| `pending`     | You submitted the job, and it is in the queue     |
| `in_progress` | The video generation is in progress               |
| `completed`   | You can download the video                        |
| `failed`      | The generation failed (examine the `error` field) |
| `cancelled`   | The job is canceled, and it did not complete      |

### Downloading the Video

When the job status is `completed`, the `unsigned_urls` array contains URLs for
the download of the generated video content. You can also use the content
endpoint directly:

```bash
curl "https://api.sume.com/v1/videos/{jobId}/content?index=0" \
  -H "Authorization: Bearer $SUME_API_KEY" \
  --output video.mp4
```

The default of the `index` query parameter is `0`. If the model generates more
than one video output, use this parameter.

## Webhooks

If you do not want to poll for the job status, you can receive a webhook
notification when a video generation job completes. Send `callback_url` in the
request body. When the job gets to a terminal state, Sume POSTs to that URL.

Sume signs the raw JSON body and sends `x-sume-webhook-timestamp` and
`x-sume-webhook-signature` headers. The payload is the standard job webhook
envelope of Sume, not the OpenRouter `video.generation.*` envelope. For the
accurate shape and the verification steps, refer to
[Sume differences](#sume-differences) and the
[webhooks guide](/api/reference).

## Sume differences

All the information above agrees with the OpenRouter Video Generation API.
This table gives the only differences.

| Area                | OpenRouter                                              | Sume                                                                                                                                                   |
| ------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Base path           | `https://openrouter.ai/api/v1/videos`                   | `https://api.sume.com/v1/videos` (no `/api` segment)                                                                                                   |
| Auth                | `Authorization: Bearer $OPENROUTER_API_KEY`             | `Authorization: Bearer $SUME_API_KEY`                                                                                                                  |
| Model ids           | `org/slug` (for example, `google/veo-3.1`)              | bare catalog IDs (for example, `seedance-2`). The published contract of Sume never has a provider-org prefix                                           |
| Auto routing        | no generate-time auto                                   | `model: "sume/auto"` lets Sume select the family. Responses show `sume/auto`. Sume never discloses the family that ran                                 |
| `size`              | accepted when the model shows `supported_sizes`         | each v1 model reports `supported_sizes: null`. Thus, `size` returns `400 unsupported_parameter`. Use `resolution` + `aspect_ratio`                     |
| `provider.options`  | OpenRouter forwards it to the matched upstream provider | v1 runs one backend for each model. Thus, a non-empty `provider.options` returns `400 unsupported_parameter`                                           |
| `seed`              | many models accept it                                   | no v1 model accepts `seed`. Each model reports `seed: false` and rejects the field                                                                     |
| Webhook envelope    | `video.generation.*` events, `X-OpenRouter-Signature`   | Sume's standard job webhook envelope with `x-sume-webhook-signature`                                                                                   |
| Idempotency         | none on this route                                      | send `Idempotency-Key` to make retries safe. A replay returns the original job                                                                         |
| Job lifecycle       | only the polling URL                                    | you can also see the same job at `GET /v1/jobs/{id}/status` and `GET /v1/jobs/{id}/result`                                                             |
| Zero Data Retention | video generation is ZDR-ineligible                      | Sume has no ZDR toggle. Refer to the [privacy docs](/the-basics)                                                                                       |
| Billing             | credits                                                 | workspace credit balance. At submit, Sume reserves the job's credits, and a failed job returns them. The job's `/v1/usage` row shows the credits used  |

### `sume/auto`

`sume/auto` is an addition that only Sume has. If you do not want to pin a
family, send it as `model`:

```json
{
  "model": "sume/auto",
  "prompt": "A vertical UGC-style product clip on a desk, natural light",
  "aspect_ratio": "9:16",
  "duration": 5
}
```

Resolution is a pure function of the normalized request and the catalog
version. Thus, an idempotent replay gets the same route and uses the same
credits. The poll response reports `"model": "sume/auto"`. Sume does not
disclose which family served the request. Do not use observable traits of the output to find the
family.

### Legacy `/v1/video-router/*`

`POST /v1/video-router/generate` and `GET /v1/video-router/models` still work
as before. They create the same jobs, with the same model IDs. The difference is
the wire. Video Router returns the `{ "data": ... }` job envelope of Sume and
accepts the flat `image_url` / `reference_image_urls` fields of Sume.
`/v1/videos` returns the response shape in the sections above.

The two APIs use the same model vocabulary. Thus, a migration is a path-and-body
change, and you do not have to map IDs again. We recommend that new
integrations use `/v1/videos`.

## Best Practices

- **Detailed Prompts**: For better video quality, write specific prompts with
  much detail. Include details of motion, camera angles, lighting, and scene
  composition.
- **Appropriate Resolution**: A higher resolution takes more time to generate
  and uses more credits. Select the resolution that is applicable to your use
  case.
- **Polling Interval**: Use a moderate polling interval (for example, 30
  seconds) to prevent too many API calls. Video generation usually takes from 30
  seconds to several minutes, as a function of the model and the parameters.
- **Error Handling**: Always examine the job status for the `failed` state.
  Make sure that your code processes the `error` field correctly.
- **Reference Images**: When you use reference images, make sure that they have
  high quality and are applicable to the video that you want.

## Troubleshooting

**Job stays in `pending` for a long time?**

- Video generation can take several minutes, as a function of the model, the
  resolution, and the server load
- Continue to poll at regular intervals

**Generation failed?**

- Examine the `error` field in the poll response for details
- Make sure that the model guidelines permit your prompt
- Make sure that all reference images are available over public HTTPS and are
  in supported formats

**Model not found?**

- Use the [Video Models API](#via-the-video-models-api) to find available video
  generation models
- Make sure that the model ID is correct (for example, `seedance-2`). Sume uses
  bare catalog IDs, not `org/slug`
