> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developers.meshapi.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developers.meshapi.ai/_mcp/server.

# Video Generation

> Generate videos from text, images, video clips, and audio using async video generation models.

# Video Generation

Mesh API provides an async video generation endpoint that accepts text prompts, reference images, video clips, and audio tracks as inputs and returns a downloadable video URL.

Video generation is asynchronous. The `POST` endpoint creates a task and returns a task ID immediately. You then poll `GET /v1/video/generations/{id}` until the task reaches a terminal state, or receive the result passively via a webhook callback.

**Base URL:** `https://api.meshapi.ai`

**Auth:** `Authorization: Bearer rsk_<your-key>` on all requests.

***

## Endpoints

| Method | Path                         | Description                    |
| :----- | :--------------------------- | :----------------------------- |
| `POST` | `/v1/video/generations`      | Create a video generation task |
| `GET`  | `/v1/video/generations/{id}` | Get task status and result     |
| `GET`  | `/v1/video/generations`      | List your tasks                |

***

## Create a Video Generation Task

`POST /v1/video/generations`

Submits a video generation request. Returns immediately with `{"id": "<task_id>"}`.

### Request body

| Field                     | Type        | Required | Description                                                                |
| :------------------------ | :---------- | :------- | :------------------------------------------------------------------------- |
| `model`                   | string      | Yes      | Model ID, e.g. `byteplus/dreamina-seedance-2-0`.                           |
| `content`                 | array       | Yes      | Array of content items (see [Content items](#content-items)).              |
| `callback_url`            | string      | No       | Your URL to receive a POST when the task completes.                        |
| `resolution`              | string      | No       | Output resolution, e.g. `720p`, `1080p`.                                   |
| `ratio`                   | string      | No       | Aspect ratio, e.g. `16:9`, `9:16`, `1:1`.                                  |
| `duration`                | integer     | No       | Target video duration in seconds.                                          |
| `frames`                  | integer     | No       | Target frame count (use `duration` or `frames`, not both).                 |
| `generate_audio`          | boolean     | No       | Generate an audio track alongside the video. Pricing differs when enabled. |
| `return_last_frame`       | boolean     | No       | Include a still image of the final frame in the result.                    |
| `seed`                    | integer     | No       | Reproducible generation seed.                                              |
| `camera_fixed`            | boolean     | No       | Lock the camera position (no camera movement).                             |
| `watermark`               | boolean     | No       | Add a provider watermark to the output.                                    |
| `draft`                   | boolean     | No       | Generate a fast draft preview.                                             |
| `service_tier`            | string      | No       | Provider service tier selection.                                           |
| `execution_expires_after` | integer     | No       | Seconds until the task expires if not completed (default 172800 = 48 h).   |
| `priority`                | integer 0–9 | No       | Task priority hint. Higher = more urgent.                                  |
| `safety_identifier`       | string      | No       | Pass a safety policy identifier for content moderation.                    |

### Content items

The `content` array lets you combine different input modalities. Each item has a `type` field and a corresponding payload field.

#### Text prompt

```json
{
  "type": "text",
  "text": "A slow-motion shot of waves crashing on a rocky coastline at sunset"
}
```

#### Image input (reference / first frame)

Pass a publicly accessible URL or a Base64-encoded image.

```json
{ "type": "image_url", "image_url": { "url": "https://example.com/reference.jpg" } }
```

```json
{ "type": "image_url", "image_url": { "url": "data:image/jpeg;base64,/9j/4AAQ..." } }
```

#### Video input (reference video)

**Seedance 2.0 series only.** Pass a publicly accessible URL or a Base64-encoded video.

```json
{ "type": "video_url", "video_url": { "url": "https://example.com/clip.mp4" } }
```

```json
{ "type": "video_url", "video_url": { "url": "data:video/mp4;base64,AAAAIGZ0eXA..." } }
```

#### Audio input

**Seedance 2.0 series only.** Pass a publicly accessible URL or a Base64-encoded audio file.

Audio cannot be used as the sole input. At least one reference image or video must also be included in the `content` array.

```json
{ "type": "audio_url", "audio_url": { "url": "https://example.com/soundtrack.mp3" } }
```

```json
{ "type": "audio_url", "audio_url": { "url": "data:audio/mpeg;base64,SUQzBA..." } }
```

### Input size limits

These limits apply to BytePlus Seedance models. Other providers may differ.

**Video input**

* Max file size: **50 MB** per video

**Audio input**

* Supported formats: `wav`, `mp3`
* Duration per clip: **2–15 seconds**; up to **3 reference audio clips** allowed, with a total combined duration of no more than **15 seconds**
* Max file size: **15 MB** per audio file
* Max total request body size: **64 MB**

Do not use Base64 encoding for large files. Use a public URL instead to stay within the 64 MB request body limit.

For public URLs, the file must be reachable by the provider's servers without authentication. For Base64 input, the data URI must include the MIME type prefix (e.g. `data:video/mp4;base64,...`).

### Seedance input support matrix

| Input type  | All Seedance models | Seedance 2.0 series only      | Public URL | Base64 |
| :---------- | :------------------ | :---------------------------- | :--------- | :----- |
| Text prompt | Yes                 | —                             | —          | —      |
| Image       | Yes                 | —                             | Yes        | Yes    |
| Video       | —                   | Yes                           | Yes        | Yes    |
| Audio       | —                   | Yes (requires image or video) | Yes        | Yes    |

### Response

```json
{ "id": "t-xxxxxxxxxxxxxxxx" }
```

Save the `id` to poll for status.

### Example — text to video

```bash
curl -X POST https://api.meshapi.ai/v1/video/generations \
  -H "Authorization: Bearer rsk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "byteplus/dreamina-seedance-2-0",
    "content": [
      {
        "type": "text",
        "text": "A cinematic drone shot over a misty mountain valley at dawn"
      }
    ],
    "resolution": "1080p",
    "ratio": "16:9",
    "duration": 5,
    "generate_audio": false,
    "return_last_frame": true,
    "callback_url": "https://yourapp.example.com/webhooks/video"
  }'
```

```python
import requests

response = requests.post(
    "https://api.meshapi.ai/v1/video/generations",
    headers={"Authorization": "Bearer rsk_your_key"},
    json={
        "model": "byteplus/dreamina-seedance-2-0",
        "content": [
            {
                "type": "text",
                "text": "A cinematic drone shot over a misty mountain valley at dawn"
            }
        ],
        "resolution": "1080p",
        "ratio": "16:9",
        "duration": 5,
        "generate_audio": False,
        "return_last_frame": True,
        "callback_url": "https://yourapp.example.com/webhooks/video"
    }
)
task_id = response.json()["id"]
print(task_id)
```

```javascript
const res = await fetch("https://api.meshapi.ai/v1/video/generations", {
  method: "POST",
  headers: {
    "Authorization": "Bearer rsk_your_key",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    model: "byteplus/dreamina-seedance-2-0",
    content: [
      {
        type: "text",
        text: "A cinematic drone shot over a misty mountain valley at dawn"
      }
    ],
    resolution: "1080p",
    ratio: "16:9",
    duration: 5,
    generate_audio: false,
    return_last_frame: true,
    callback_url: "https://yourapp.example.com/webhooks/video"
  })
});
const { id } = await res.json();
console.log(id);
```

### Example — image to video

```json
{
  "model": "byteplus/dreamina-seedance-2-0",
  "content": [
    {
      "type": "image_url",
      "image_url": { "url": "https://example.com/first-frame.jpg" }
    },
    {
      "type": "text",
      "text": "The subject slowly turns and walks toward the camera"
    }
  ],
  "duration": 5,
  "ratio": "9:16"
}
```

### Example — video with audio input

```json
{
  "model": "byteplus/dreamina-seedance-2-0",
  "content": [
    {
      "type": "text",
      "text": "A product reveal with dramatic lighting"
    },
    {
      "type": "audio_url",
      "audio_url": { "url": "https://example.com/background-music.mp3" }
    }
  ],
  "duration": 8,
  "generate_audio": false
}
```

***

## Get Task Status

`GET /v1/video/generations/{id}`

Returns the current state of a task. For non-terminal tasks, Mesh API fetches the latest status from the upstream provider and syncs its database before responding.

### Task statuses

| Status      | Meaning                                      |
| :---------- | :------------------------------------------- |
| `queued`    | Task accepted, not yet started.              |
| `running`   | Generation in progress.                      |
| `succeeded` | Complete — `content.video_url` is populated. |
| `failed`    | Error — check the `error` field.             |
| `expired`   | Task timed out before completing.            |
| `cancelled` | Task was cancelled.                          |

Once a task reaches a terminal status (`succeeded`, `failed`, `expired`, `cancelled`), subsequent GET calls are served from the Mesh API database without hitting the upstream provider.

### Response fields

| Field                     | Type    | Description                                                        |
| :------------------------ | :------ | :----------------------------------------------------------------- |
| `id`                      | string  | Task ID.                                                           |
| `model`                   | string  | Model ID used.                                                     |
| `status`                  | string  | Current task status.                                               |
| `content.video_url`       | string  | Downloadable video URL (when `succeeded`).                         |
| `content.last_frame_url`  | string  | Still image of the final frame (when `return_last_frame` was set). |
| `error.code`              | string  | Error code (when `failed`).                                        |
| `error.message`           | string  | Human-readable error description (when `failed`).                  |
| `usage.completion_tokens` | integer | Tokens billed for the generation.                                  |
| `usage.total_tokens`      | integer | Total tokens (input + output).                                     |
| `seed`                    | integer | Seed used for the generation.                                      |
| `resolution`              | string  | Output resolution.                                                 |
| `ratio`                   | string  | Aspect ratio.                                                      |
| `duration`                | integer | Duration in seconds.                                               |
| `frames`                  | integer | Frame count.                                                       |
| `framespersecond`         | integer | Frames per second of the output.                                   |
| `generate_audio`          | boolean | Whether audio was generated.                                       |
| `created_at`              | integer | Unix timestamp of task creation.                                   |
| `updated_at`              | integer | Unix timestamp of last status update.                              |

### Example — polling until complete

```python
import time
import requests

TASK_ID = "t-xxxxxxxxxxxxxxxx"
headers = {"Authorization": "Bearer rsk_your_key"}

while True:
    res = requests.get(
        f"https://api.meshapi.ai/v1/video/generations/{TASK_ID}",
        headers=headers
    ).json()

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

    if status == "succeeded":
        print("Video URL:", res["content"]["video_url"])
        break
    elif status in ("failed", "expired", "cancelled"):
        print("Error:", res.get("error"))
        break

    time.sleep(5)
```

```bash
# Poll until terminal
while true; do
  RESULT=$(curl -s "https://api.meshapi.ai/v1/video/generations/$TASK_ID" \
    -H "Authorization: Bearer rsk_your_key")
  STATUS=$(echo $RESULT | jq -r '.status')
  echo "Status: $STATUS"
  if [[ "$STATUS" == "succeeded" || "$STATUS" == "failed" || "$STATUS" == "expired" ]]; then
    echo $RESULT | jq
    break
  fi
  sleep 5
done
```

### Succeeded response

```json
{
  "id": "t-xxxxxxxxxxxxxxxx",
  "model": "byteplus/dreamina-seedance-2-0",
  "status": "succeeded",
  "content": {
    "video_url": "https://cdn.example.com/videos/generated.mp4",
    "last_frame_url": "https://cdn.example.com/videos/last-frame.jpg"
  },
  "usage": {
    "completion_tokens": 200,
    "total_tokens": 200
  },
  "resolution": "1080p",
  "ratio": "16:9",
  "duration": 5,
  "frames": 150,
  "framespersecond": 30,
  "created_at": 1718000000,
  "updated_at": 1718000180
}
```

### Failed response

```json
{
  "id": "t-xxxxxxxxxxxxxxxx",
  "model": "byteplus/dreamina-seedance-2-0",
  "status": "failed",
  "error": {
    "code": "content_policy_violation",
    "message": "The request could not be processed due to content policy."
  }
}
```

***

## List Tasks

`GET /v1/video/generations`

Returns tasks stored in Mesh API's database for your API key. This endpoint does **not** refresh task status from the upstream provider — use `GET /v1/video/generations/{id}` to force a live sync for an in-progress task.

### Query parameters

| Parameter        | Type                | Description                                                                                 |
| :--------------- | :------------------ | :------------------------------------------------------------------------------------------ |
| `status`         | string (repeatable) | Filter by status. Multiple values combine with OR, e.g. `?status=running&status=succeeded`. |
| `model`          | string (repeatable) | Filter by model ID. Multiple values combine with OR.                                        |
| `created_after`  | ISO 8601 datetime   | Only include tasks created at or after this timestamp.                                      |
| `created_before` | ISO 8601 datetime   | Only include tasks created before this timestamp.                                           |
| `limit`          | integer 1–200       | Page size (default 50).                                                                     |
| `offset`         | integer             | Pagination offset (default 0).                                                              |

Results are ordered by `created_at` descending (newest first).

### Response

```json
{
  "object": "list",
  "data": [
    {
      "id": "t-xxxxxxxxxxxxxxxx",
      "model": "byteplus/dreamina-seedance-2-0",
      "status": "succeeded",
      "created_at": 1718000000,
      "updated_at": 1718000180,
      "content": {
        "video_url": "https://cdn.example.com/videos/generated.mp4"
      }
    }
  ],
  "has_more": false,
  "total": 1,
  "limit": 50,
  "offset": 0
}
```

***

## Webhooks

Instead of polling, you can pass a `callback_url` in the create request. Mesh API will `POST` the full task payload to your URL when the task reaches a terminal status (`succeeded`, `failed`, or `expired`).

### How it works

1. You pass `"callback_url": "https://yourapp.example.com/webhooks/video"` in the create request.
2. Mesh API intercepts the completion notification from the upstream provider, updates its database, and then forwards the completed task payload to your `callback_url`.
3. Your endpoint receives a `POST` with a JSON body containing the final task state.

This means you never need to poll — just register a webhook endpoint and handle the event when it arrives.

### Webhook payload

The payload is identical to the response from `GET /v1/video/generations/{id}`:

```json
{
  "id": "t-xxxxxxxxxxxxxxxx",
  "model": "byteplus/dreamina-seedance-2-0",
  "status": "succeeded",
  "content": {
    "video_url": "https://cdn.example.com/videos/generated.mp4",
    "last_frame_url": "https://cdn.example.com/videos/last-frame.jpg"
  },
  "usage": {
    "completion_tokens": 200,
    "total_tokens": 200
  },
  "generate_audio": false,
  "resolution": "1080p",
  "ratio": "16:9",
  "duration": 5
}
```

### Handling the webhook

Your endpoint should:

1. Return a `2xx` response quickly. Mesh API fires the callback as fire-and-forget with a 10-second timeout — a slow response won't block task completion on our side, but it will not be retried.
2. Identify the task from `id` in the payload.
3. Check `status` to decide what to do — download `content.video_url` on success, log or surface `error` on failure.

```python
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route("/webhooks/video", methods=["POST"])
def video_webhook():
    task = request.json
    task_id = task["id"]
    status = task["status"]

    if status == "succeeded":
        video_url = task["content"]["video_url"]
        # save or process the video
        print(f"Task {task_id} succeeded: {video_url}")
    elif status == "failed":
        error = task.get("error", {})
        print(f"Task {task_id} failed: {error.get('code')} — {error.get('message')}")
    elif status == "expired":
        print(f"Task {task_id} expired before completing")

    return jsonify({"ok": True}), 200
```

### Webhook vs polling — when to use each

| Approach    | Best for                                                                       |
| :---------- | :----------------------------------------------------------------------------- |
| **Webhook** | Production apps, background pipelines — no polling overhead, event-driven.     |
| **Polling** | Quick scripts, testing, environments where you can't expose a public endpoint. |

You can use both simultaneously: pass a `callback_url` for production delivery and still poll `GET /v1/video/generations/{id}` as a fallback. Mesh API deduplicates usage logging so you are never billed twice.

***

## Pricing

Video generation pricing is token-based and varies by the type of output:

| Output type                            | Billing basis                               |
| :------------------------------------- | :------------------------------------------ |
| Video only                             | `completion_tokens` from the usage response |
| Video + audio (`generate_audio: true`) | Higher per-token rate                       |
| Video with video input                 | Higher per-token rate                       |

The exact per-token rates for each model are listed on the [Pricing](./pricing) page.

***

## Error reference

| HTTP status | Meaning                                           |
| :---------- | :------------------------------------------------ |
| `200`       | Task created or retrieved successfully.           |
| `401`       | Missing or invalid API key.                       |
| `402`       | Insufficient balance or spend cap reached.        |
| `422`       | The model is not supported for video generation.  |
| `429`       | Rate limit exceeded.                              |
| `502`       | Upstream provider error.                          |
| `503`       | Video generation service temporarily unavailable. |

When a task itself fails (HTTP 200, `status: "failed"`), the `error.code` and `error.message` fields describe the reason from the provider (e.g. `content_policy_violation`, `invalid_input`).