# Image-to-video

> Animate a starting image into video by controlling the first frame: submit, poll, and download across the unified, OpenAI/Sora, Kling, and Jimeng routes.

Animate a still image into an MP4 by passing it as the first frame of a video-generation task. This page walks through a runnable Python script plus the parameters, routes, and error cases that matter for image-to-video.

## Before you start

- `pip install requests`
- `MIRAPI_API_KEY` exported as an environment variable
- A video-capable model enabled for your key
- A first-frame image reachable as an HTTP(S) URL or base64 data URI

:::tip[Pick a video model]
Video endpoints only accept models with video output. `deepseek-chat` in the examples below is a placeholder — replace it with a video model's exact `id` from your catalogue.
:::

## How image-to-video works

Video generation is **asynchronous**: the API returns a task right away, and you poll until the clip is ready.

1. `POST /v1/video/generations` — submit the job and receive a `task_id`.
2. `GET /v1/video/generations/{task_id}` — poll until `status` is `completed` or `failed`.
3. Download the MP4 from the `url` in the completed task.

The `image` field is what makes a request image-to-video: the model uses it as the first frame and keeps the composition, so you describe the **motion** in `prompt` instead of re-describing the scene.

## The script

```python
import base64
import os
import time
import requests

BASE = "https://api.mirapi.ai/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['MIRAPI_API_KEY']}"}
POLL_INTERVAL = 10
DEADLINE_SECONDS = 600


def image_to_data_uri(path: str) -> str:
    mime = "image/png" if path.endswith(".png") else "image/jpeg"
    with open(path, "rb") as f:
        return f"data:{mime};base64,{base64.b64encode(f.read()).decode()}"


# Pass an HTTP(S) URL, or convert a local file to a data URI:
first_frame = "https://example.com/first-frame.png"
# first_frame = image_to_data_uri("first-frame.png")

resp = requests.post(
    f"{BASE}/video/generations",
    headers=HEADERS,
    json={
        "model": "deepseek-chat",  # replace with a video model id
        "prompt": "The photo comes to life; the person waves at the camera",
        "image": first_frame,
        "duration": 5,
    },
    timeout=60,
)
resp.raise_for_status()
task_id = resp.json()["task_id"]

deadline = time.time() + DEADLINE_SECONDS
while time.time() < deadline:
    task = requests.get(
        f"{BASE}/video/generations/{task_id}", headers=HEADERS, timeout=30
    ).json()
    if task["status"] == "completed":
        break
    if task["status"] == "failed":
        raise SystemExit(f"Video failed: {task.get('error')}")
    time.sleep(POLL_INTERVAL)
else:
    raise TimeoutError(f"Task {task_id} did not finish within {DEADLINE_SECONDS}s")

video = requests.get(task["url"], timeout=60)
video.raise_for_status()
with open("animated.mp4", "wb") as f:
    f.write(video.content)
print("Saved animated.mp4")
```

The script submits once, then polls every 10 seconds with a 10-minute deadline. It exits on a `failed` status, raises on timeout, and otherwise downloads the completed task's `url`.

## First-frame tips

- Hosted images take an **HTTP(S) URL**; local files become a **base64 data URI** — both are accepted.
- Describe the **motion** in `prompt`, not the scene. The model already has the first frame's composition.
- `duration` is billed output; start with 5 seconds and extend only if the model supports longer clips.
- Make sure the image URL is directly downloadable — a plain image response, not an HTML page, redirect wall, or login page.

## Request parameters

The unified and Kling routes accept the same JSON body:

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | yes | Video-capable model `id` from the catalogue. |
| `prompt` | string | yes | Text description of the motion to animate. |
| `image` | string | yes | First-frame image: HTTP(S) URL or base64 data URI. |
| `duration` | number | no | Target length in seconds. |
| `width` | integer | no | Output width in pixels. |
| `height` | integer | no | Output height in pixels. |
| `fps` | integer | no | Frame rate. |
| `seed` | integer | no | Seed for reproducible output (not guaranteed by every model). |
| `n` | integer | no | Number of videos to generate. |
| `response_format` | string | no | Response format (e.g. `url`). |
| `user` | string | no | End-user identifier for abuse tracking. |
| `metadata` | object | no | Model-specific extras (`negative_prompt`, `style`, `quality_level`, …) where the model accepts them. |

`image` is the field that turns the request into image-to-video; omit it and the same route behaves as text-to-video.

## Task status

`status` is one of `queued`, `in_progress`, `completed`, `failed`. A completed task carries the video `url`, `format` (e.g. `mp4`), and a `metadata` object with the actual `duration`, `width`, `height`, and `fps`; a failed task includes an `error`.

```json
{
  "task_id": "abcd1234efgh",
  "status": "completed",
  "url": "https://…",
  "format": "mp4",
  "metadata": { "duration": 5, "width": 1280, "height": 720 }
}
```

Poll every 10–30 seconds — a job can take minutes, depending on the model and resolution.

## Endpoint routes

Video generation is **OpenAI-compatible only** — there is no Anthropic or Gemini protocol variant. MirAPI exposes four route families that all follow submit → poll → download, but differ in request format and response shape.

### Unified format

- `POST /v1/video/generations` — JSON body (the request parameters above). Returns `task_id` and `status`.
- `GET /v1/video/generations/{task_id}` — status; a completed task carries `url`, `format`, and `metadata`.

### OpenAI / Sora format

- `POST /v1/videos` — `multipart/form-data` with `model`, `prompt`, `seconds` (a string), and an `input_reference` image file. Returns an `id` (not `task_id`).
- `GET /v1/videos/{task_id}` — task status, including `progress` and `created_at`.
- `GET /v1/videos/{task_id}/content` — the finished file, returned as `video/mp4`.

### Kling format

- `POST /kling/v1/videos/image2video` — the same JSON body as the unified route; takes `image` for the first frame.
- `GET /kling/v1/videos/image2video/{task_id}` — poll status. The `text2video` route (`/kling/v1/videos/text2video`) shares this shape without `image`.

### Jimeng format

- `POST /jimeng/` — the native Jimeng API. `Action` and `Version` are query parameters: `Action` is `CVSync2AsyncSubmitTask` (submit) or `CVSync2AsyncGetResult` (fetch result), and `Version` is the API version (e.g. `2022-08-31`). The JSON body carries `req_key`, `prompt`, and an optional `binary_data_base64` image array.

:::note
Only the OpenAI/Sora route uses multipart; the unified, Kling, and Jimeng routes accept JSON.
:::

## Errors and troubleshooting

| Status | Meaning | What to do |
| --- | --- | --- |
| `400` | Invalid parameters | Check the model `id`, and that `duration`/`width`/`height` are numeric. |
| `401` | Missing or invalid key | Verify `Authorization: Bearer`. |
| `403` | Key valid but rejected | Top up (empty balance), or check the model whitelist, quota, and IP allowlist. |
| `404` | Task or content not found | The task does not exist, or the video is not `completed` yet. |
| `429` | Rate limited | Back off exponentially (1s → 2s → 4s, cap ~30s); there is no `Retry-After` header. |
| `500` | Gateway or upstream failure | Retry idempotent requests safely. |

Every error message ends with a request ID — include it in support tickets.

- **Task stays `queued` or `in_progress`** — normal; jobs can take minutes. Keep polling at a steady interval.
- **Generation `failed`** — read the `error` field, confirm the model has video output, and check that the reference image is reachable and in a supported format.
- **`404` on the content endpoint** — the task is not `completed` yet; poll the status endpoint first.

## Approximate cost

Video is billed **per second of generated output**, settled when the job completes. Failed jobs produce no output and are not charged.

- Balance exhausted → the request fails with `403`; top up in the console and the same key recovers immediately.
- Reconcile spend in the console billing log or with `GET /api/log/token` (per-request detail).

## Related links

- [Video generation guide](/docs/guides/video) — full reference for models, parameters, and routes
- [Text-to-video tutorial](/docs/tutorials/text-to-video) — submit/poll/download script with timeout and failure branches
- [Models & pricing](/docs/models) — find video-capable models and per-second pricing
- [Billing & top-ups](/docs/billing) — how balance, top-ups, and reconciliation work