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

# Video Generation

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

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 theme={null}
{
  "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 theme={null}
{ "type": "image_url", "image_url": { "url": "https://example.com/reference.jpg" } }
```

```json theme={null}
{ "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 theme={null}
{ "type": "video_url", "video_url": { "url": "https://example.com/clip.mp4" } }
```

```json theme={null}
{ "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.

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

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

```json theme={null}
{ "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: 32 MiB (33,554,432 bytes).** This is a hard platform
limit on every Mesh endpoint, enforced at the edge — an oversized request is rejected
before it reaches the API and returns a plain HTML `413 Request Entity Too Large`
page rather than a Mesh error envelope. See [Rate Limits & Spend Caps](/docs/getting-started/rate-limits#request-body-size).

<Warning>
  Do not use Base64 encoding for large files — it inflates the payload by \~33%, so a
  file of \~24 MB already exceeds the 32 MiB request body limit on its own. Use a public
  URL instead; the per-file caps above are only reachable that way.
</Warning>

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,...`).

### Character consistency on Seedance 2.x

Seedance 2.0 and 2.5 screen reference material for likeness and copyright risk, and
will decline a reference that looks like a real person. That screening is the reason a
reference which "should" work sometimes produces a video ignoring it.

For these models, Mesh registers your reference with the provider's trusted asset
library before generating, so a consistent character survives across shots. This is
automatic — send the reference exactly as you would otherwise, as a public URL or a
data URI, and Mesh handles registration.

Three consequences worth knowing:

<Note>
  **The first request using a new reference can return `422` with "still being
  prepared".** Registration is asynchronous on the provider's side and has no
  guaranteed completion time. Retry the same request; the reference is recognised by
  content, so the retry reuses the in-flight registration rather than starting another
  one. Subsequent requests with the same image are immediate.
</Note>

**References are matched on content, not URL.** The same image behind two different
URLs — a re-signed link, a CDN variant — is one reference. Changing the URL of an
image you have already used costs nothing.

**`asset://` references are not accepted.** If you have seen provider asset IDs
elsewhere, they cannot be passed to Mesh; supply the reference as a URL and Mesh
registers it under your organisation. Sending one returns `422`.

<Warning>
  By supplying a reference you confirm you have the rights to it and that it is not the
  likeness of a real person. Registered references are retained while you keep using
  them and are removed once unused. See the [Terms of Service](https://meshapi.ai/terms).
</Warning>

### 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 theme={null}
{ "id": "t-xxxxxxxxxxxxxxxx" }
```

Save the `id` to poll for status.

### Example — text to video

<Tabs>
  <Tab title="curl">
    ```bash theme={null}
    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"
      }'
    ```
  </Tab>

  <Tab title="Python">
    ```python theme={null}
    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)
    ```
  </Tab>

  <Tab title="Node.js">
    ```javascript theme={null}
    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);
    ```
  </Tab>
</Tabs>

### Example — image to video

```json theme={null}
{
  "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 theme={null}
{
  "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

<Tabs>
  <Tab title="Python">
    ```python theme={null}
    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)
    ```
  </Tab>

  <Tab title="curl">
    ```bash theme={null}
    # 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
    ```
  </Tab>
</Tabs>

### Succeeded response

```json theme={null}
{
  "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 theme={null}
{
  "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 theme={null}
{
  "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 theme={null}
{
  "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 theme={null}
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](/docs/getting-started/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`).
