Skip to content

Image-to-video

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

  • 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

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.

Adding the image field makes the request an image-to-video job. The model uses the image as the first frame and keeps its composition, so you describe the motion in prompt instead of re-describing the scene.

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.

  • The first-frame image can be an HTTP(S) URL for hosted images or a base64 data URI for local files.
  • Describe the motion in prompt, not the scene. The model already has the first frame’s composition.
  • You are billed for the duration of the output. Start with 5 seconds and go longer only if the model supports longer clips.
  • Make sure the image URL is directly downloadable. It should return the image itself, not an HTML page, a redirect page, or a login page.

The unified and Kling routes accept the same JSON body:

Parameter Type Required Description
model string yes Video-capable model id from the catalog.
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.

With image, the request is an image-to-video job; without it, the same route does text-to-video.

status is one of queued, in_progress, completed, or 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.

{
"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.

Video generation is OpenAI-compatible only; there is no Anthropic or Gemini protocol variant. MirAPI exposes four route families. All of them follow submit → poll → download, but they differ in request format and response shape.

  • POST /v1/video/generations: JSON body (the request parameters above). Returns task_id and status.
  • GET /v1/video/generations/{task_id}: task status. A completed task carries url, format, and metadata.
  • 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.
  • POST /kling/v1/videos/image2video: the same JSON body as the unified route, with image as the first frame.
  • GET /kling/v1/videos/image2video/{task_id}: poll status. The text2video route (/kling/v1/videos/text2video) shares this shape without image.
  • POST /jimeng/: the native Jimeng API. Pass Action and Version as 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.
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 Idempotent requests are safe to retry.

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

  • Task stays queued or in_progress: this is normal, since 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.

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

  • If the balance runs out, the request fails with 403. Top up in the Platform and the same key works again immediately.
  • Reconcile spend in the Platform billing log or with GET /api/log/token (per-request detail).