Image to Video API Integration Guide
Version: V1 | Last updated: November 2, 2026
Image to Video (image-to-video) generates motion from input images. The current input configuration accepts one or two images for single-image generation or a first/last-frame transition. Use a prompt to describe the camera motion and action.
This guide describes production API authentication, request parameters, task queries, and integration examples in Python, Go, PHP, and Java. Replace the media URLs, credentials, and task IDs in the examples with your own values.
Contents
- Getting Started
- Submit a Task
- Response Format
- Query a Task
- Language Examples
- Request Examples
- Related Documentation
Getting Started
Get Your AK and SK
-
Sign in to the MeituHub Console and open Account Overview.
-
In the API Keys section, click Copy next to each of the following fields to obtain your AK and SK:
MEITU_OPENAPI_ACCESS_KEY: Access Key (AK).MEITU_OPENAPI_SECRET_KEY: Secret Key (SK).
Use the SK only for server-side request signing. Do not include it in browser code, mobile apps, public repositories, or logs.
Request Signing
Both submission and query requests require AK/SK signing. Use the official signing SDK to sign the full URL, HTTP method, headers, and request body, then send the request generated by the SDK. Do not modify the body after signing.
For signing details, see API Request Signing.
Submit a Task
POST https://openapi.meitu.com/api/v1/sdk/sync/push
Content-Type: application/jsonRequest Parameters
The params.parameter.* paths below refer to business fields inside the decoded params string. Place them in a parameter object and serialize the entire JSON object containing it into the params string.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| task | string | Yes | Fixed value | Capability identifier. Always image-to-video. |
| task_type | string | Yes | Fixed value | Always formula. |
| init_images | object[] | Yes | None | Input image list, currently one or two images. Use init_images[0] for a single image, or first frame followed by last frame for a transition. |
| params | string | Yes | None | A JSON string containing a parameter object with the fields listed below. |
| params.parameter.prompt | string | Yes | None | Video motion prompt. Describe camera movement, subject actions, and the intended scene. |
| params.parameter.mode | string | No | auto | Generation mode: auto for automatic selection, i2v for single-image generation, mi2v for image-reference fusion, and flf for first/last-frame transitions. The current entry point still accepts at most two images when mi2v is selected. Values: auto, i2v, mi2v, flf. |
| params.parameter.model | string | No | auto | Generation model. auto selects automatically. Use mode/model combinations supported by the execution channel. Values: auto, toffee, bonbon. |
| params.parameter.aspect_ratio | string | No | No fixed default configured | Aspect ratio as free text, such as 16:9. No fixed enum is configured. Supported ratios depend on the selected mode and model. |
| params.parameter.video_duration | number | No | No fixed default configured | Video duration in seconds, supplied as a JSON number, for example 5. Supported ranges depend on the selected mode and model. |
| params.parameter.sound | string | No | No fixed default configured | Whether to generate audio. Values: on, off. |
| params.parameter.multi_shot | string | No | No fixed default configured | Whether to enable multiple shots. The current parameter schema defines this field as a string. Omit when not needed; support depends on the execution channel. |
| sync_timeout | integer | No | 30 in these examples | Synchronous wait time in seconds. Retain the task ID and query the task after a timeout. |
For this API, video_duration is a JSON number. No fixed defaults are configured for aspect_ratio, video_duration, sound, or multi_shot; omit them when not needed. Supported combinations of mode, model, image count, and output settings vary by execution channel.
Media Input
Each element in init_images uses this structure:
{
"url": "https://example.com/input.jpg",
"profile": {
"version": "v1",
"media_profiles": {
"media_data_type": "url"
}
}
}| Field | Type | Required | Description |
|---|---|---|---|
| url | string | Yes | An image URL directly accessible to the server. |
| profile.version | string | Yes | Always v1. |
| profile.media_profiles.media_data_type | string | Yes | Always url. |
Send one image for single-image generation. For a transition, send two images with mode=flf: init_images[0] is the first frame and init_images[1] is the last frame. The current entry point accepts at most two images.
Business Parameters
Place the business fields in a parameter object, then serialize the entire object into the params string. Example before serialization:
{
"parameter": {
"prompt": "Slow camera push-in with natural subject motion and stable framing",
"mode": "i2v",
"model": "auto",
"video_duration": 5,
"sound": "off",
"aspect_ratio": "16:9"
}
}The corresponding params field in the request body:
{
"params": "{\"parameter\":{\"prompt\":\"Slow camera push-in with natural subject motion and stable framing\",\"mode\":\"i2v\",\"model\":\"auto\",\"video_duration\":5,\"sound\":\"off\",\"aspect_ratio\":\"16:9\"}}"
}The outer type of params must be a string. Match the field names, nesting, types, and enum case shown in the request parameter table.
Response Format
Response Fields
Gateway responses may include an envelope containing code, error_code, message, and data. When the envelope is present, check the outer business codes first, then read the task object from data. If an SDK returns the task object directly, read status, task_id, result, and related fields at the top level.
| Field | Type | Description |
|---|---|---|
| code | integer, optional | Gateway business code. 0 indicates normal request processing. For nonzero values, inspect the error details. |
| error_code | integer, optional | Gateway error code. 0 indicates no corresponding gateway error. |
| message | string, optional | Gateway message or error details. |
| request_id / trace_id | string, optional | Request tracing identifiers. Retain them for troubleshooting. |
| data | object, optional | Task object. Absent when the task object is returned directly. |
The following paths are relative to the task object:
| Field | Type | Description |
|---|---|---|
| status | integer | Task status. See the status table below. |
| task_id | string | Task ID used for subsequent queries. |
| progress | number, optional | Task progress, usually from 0 to 1. |
| result | object / null | Task result. May be empty before completion or on failure. |
| result.id | string, optional | Task ID associated with the result. |
| result.media_info_list | object[], optional | Output video list. |
| result.media_info_list[].media_data | string | Output video URL. |
| result.media_info_list[].media_profiles | object, optional | Media metadata. For URL results, media_data_type is url. |
| result.media_info_list[].media_extra | object / null, optional | Additional media metadata. |
| result.parameters | JSON value, optional | Additional capability-specific business data. |
| result.code / result.error_code | integer, optional | Business error codes in the result. Inspect the error details for nonzero values. |
| result.msg / result.error_msg | string, optional | Result message or error details. |
Task Status
| status | Meaning | Action |
|---|---|---|
| -1 | Task not found | Check the task ID, calling account, and task retention period. |
| 0 | Created | Save the task ID and continue querying. |
| 1 | Running | Continue querying. |
| 2 | Failed | Handle the gateway and result error details. |
| 9 | Synchronous wait timed out | Save the task ID and continue querying. |
| 10 | Succeeded | Read result.media_info_list. |
HTTP 200 only indicates that the HTTP request completed. Determine task success using the outer business codes, task status, and result errors. Do not treat progress=1 or the presence of a task_id alone as success.
Success Example
This example shows the response structure with a gateway envelope. The task ID and media URL are placeholders:
{
"code": 0,
"error_code": 0,
"message": "success",
"data": {
"progress": 1,
"result": {
"id": "example-task-id",
"media_info_list": [
{
"media_data": "https://example.com/result.mp4",
"media_extra": {},
"media_profiles": {
"media_data_type": "url"
}
}
],
"parameters": {}
},
"status": 10,
"task_id": "example-task-id"
}
}With a data envelope, the first output video URL is data.result.media_info_list[0].media_data. For a directly returned task object, it is result.media_info_list[0].media_data. Process every item in the returned list and download the result files promptly.
Synchronous Timeout Example
{
"code": 0,
"error_code": 0,
"message": "success",
"data": {
"progress": 0,
"result": {
"id": "example-task-id"
},
"status": 9,
"task_id": "example-task-id"
}
}status=9 does not mean the task failed. Query the result using the returned task_id.
Failure Example
{
"code": 0,
"error_code": 0,
"message": "success",
"data": {
"progress": 0,
"result": null,
"status": 2,
"task_id": "example-task-id"
}
}A task can fail with status=2 even when the outer business codes are 0. Authentication failures, rate limits, and invalid parameters may instead return nonzero business codes or a non-2xx HTTP status without a task ID. Retain the error response and request tracing identifiers. For general error codes, see API Error Codes.
Query a Task
GET https://openapi.meitu.com/api/v1/sdk/status?task_id=example-task-id| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
| task_id | Query | string | Yes | Task ID returned by submission. |
Use AK/SK credentials from the same account used for submission. Sign the full URL including task_id and the GET method, with an empty request body. The returned fields and status meanings are described in Response Format.
Increase query intervals gradually, for example 2, 5, then 10 seconds, and set an application-level waiting limit. If that limit is reached, retain the task ID and query again later. Task information and media links may expire, so query and save results promptly.
If a submission request times out before you receive a task ID, verify whether the task was created before submitting again.
Language Examples
The four examples use the same request.json file. Copy the complete request body from Request Examples, replace the media URLs and other placeholders, and save it as request.json.
SDK Setup
| Language | Official Documentation | Dependencies |
|---|---|---|
| Python | Python SDK | sign_sdk, requests |
| Go | Go SDK | github.com/mtlab/api/signer |
| PHP | PHP SDK | Official signer.php, cURL extension, PHP 7.3+ |
| Java | Java SDK | Official signing SDK, JDK 11+ |
Download the Python signing package from the SDK download link. Add the extracted sign_sdk package to your import path and install requests:
python -m pip install requestsRun Configuration
To submit a task:
export MEITU_OPENAPI_ACTION='push'
export MEITU_REQUEST_FILE='request.json'To query a task, use this configuration instead:
export MEITU_OPENAPI_ACTION='status'
export MEITU_TASK_ID='example-task-id'Each run performs the selected operation once and prints the full HTTP response. The examples do not automatically poll or retry submissions. After receiving a response, check the business codes and task status as described in Response Format.
Python
Save as client.py and run python client.py:
import json
import os
from pathlib import Path
from urllib.parse import urlencode
import requests
from sign_sdk import sign
HOST = "openapi.meitu.com"
ACTION = os.environ.get("MEITU_OPENAPI_ACTION", "push")
ak = os.environ["MEITU_OPENAPI_ACCESS_KEY"]
sk = os.environ["MEITU_OPENAPI_SECRET_KEY"]
if not ak or not sk:
raise ValueError("Missing AK/SK")
if ACTION == "push":
payload = json.loads(Path(os.environ.get("MEITU_REQUEST_FILE", "request.json")).read_text(encoding="utf-8"))
if not isinstance(payload.get("params"), str):
raise ValueError("params must be a JSON string")
json.loads(payload["params"])
method = "POST"
url = f"https://{HOST}/api/v1/sdk/sync/push"
body = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
elif ACTION == "status":
task_id = os.environ["MEITU_TASK_ID"]
if not task_id:
raise ValueError("Missing task_id")
method = "GET"
url = f"https://{HOST}/api/v1/sdk/status?" + urlencode({"task_id": task_id})
body = ""
else:
raise ValueError("MEITU_OPENAPI_ACTION must be push or status")
headers = {"Content-Type": "application/json", sign.HeaderHost: HOST}
signed_request = sign.Signer(ak, sk).sign(url, method, headers, body)
with requests.Session() as session:
response = session.send(signed_request, timeout=(10, 100), verify=True, allow_redirects=False)
print("HTTP", response.status_code)
print(response.text)
response.raise_for_status()Go
Save as main.go and run go run main.go in a Go module with the signing SDK dependency configured:
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
"github.com/mtlab/api/signer"
)
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func run() error {
ak, sk := os.Getenv("MEITU_OPENAPI_ACCESS_KEY"), os.Getenv("MEITU_OPENAPI_SECRET_KEY")
if ak == "" || sk == "" {
return fmt.Errorf("missing AK/SK")
}
method, endpoint, body := http.MethodPost, "https://openapi.meitu.com/api/v1/sdk/sync/push", ""
switch os.Getenv("MEITU_OPENAPI_ACTION") {
case "", "push":
filename := os.Getenv("MEITU_REQUEST_FILE")
if filename == "" {
filename = "request.json"
}
raw, err := os.ReadFile(filename)
if err != nil {
return err
}
var payload map[string]interface{}
if err = json.Unmarshal(raw, &payload); err != nil {
return err
}
params, ok := payload["params"].(string)
if !ok || !json.Valid([]byte(params)) {
return fmt.Errorf("params must be a JSON string")
}
body = string(raw)
case "status":
taskID := os.Getenv("MEITU_TASK_ID")
if taskID == "" {
return fmt.Errorf("missing task_id")
}
method = http.MethodGet
endpoint = "https://openapi.meitu.com/api/v1/sdk/status?" + url.Values{"task_id": {taskID}}.Encode()
default:
return fmt.Errorf("MEITU_OPENAPI_ACTION must be push or status")
}
headers := make(http.Header)
headers.Set("Content-Type", "application/json")
headers.Set(signer.HeaderHost, "openapi.meitu.com")
req, err := signer.NewSigner(ak, sk).Sign(endpoint, method, headers, body)
if err != nil {
return err
}
client := &http.Client{Timeout: 100 * time.Second, CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
fmt.Println("HTTP", resp.StatusCode)
fmt.Println(string(raw))
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("HTTP request failed")
}
return nil
}PHP
Place the official signer.php in the same directory, save as client.php, and run php client.php:
<?php
require_once __DIR__ . '/signer.php';
$ak = getenv('MEITU_OPENAPI_ACCESS_KEY');
$sk = getenv('MEITU_OPENAPI_SECRET_KEY');
if (!$ak || !$sk) { throw new RuntimeException('Missing AK/SK'); }
$action = getenv('MEITU_OPENAPI_ACTION') ?: 'push';
$body = '';
if ($action === 'push') {
$file = getenv('MEITU_REQUEST_FILE') ?: 'request.json';
$body = file_get_contents($file);
if ($body === false) { throw new RuntimeException('Cannot read request.json'); }
$payload = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
if (!isset($payload['params']) || !is_string($payload['params'])) { throw new RuntimeException('params must be a JSON string'); }
json_decode($payload['params'], true, 512, JSON_THROW_ON_ERROR);
$method = 'POST';
$url = 'https://openapi.meitu.com/api/v1/sdk/sync/push';
} elseif ($action === 'status') {
$taskId = getenv('MEITU_TASK_ID');
if (!$taskId) { throw new RuntimeException('Missing task_id'); }
$method = 'GET';
$url = 'https://openapi.meitu.com/api/v1/sdk/status?' . http_build_query(['task_id' => $taskId], '', '&', PHP_QUERY_RFC3986);
} else {
throw new RuntimeException('MEITU_OPENAPI_ACTION must be push or status');
}
$headers = ['Content-Type' => 'application/json', 'Host' => 'openapi.meitu.com'];
$curl = (new Signer($ak, $sk))->sign($url, $method, $headers, $body);
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => false,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 100,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
]);
$response = curl_exec($curl);
if ($response === false) {
$error = curl_error($curl);
curl_close($curl);
throw new RuntimeException($error);
}
$status = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
echo 'HTTP ' . $status . PHP_EOL . $response . PHP_EOL;
if ($status < 200 || $status >= 300) { throw new RuntimeException('HTTP request failed'); }Java
Add the official SDK to the classpath, save as Main.java, and compile and run with JDK 11+. Encode request.json as UTF-8:
import com.meitu.openai.common.Signer;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
public class Main {
private static String requiredEnv(String name) {
String value = System.getenv(name);
if (value == null || value.isBlank()) throw new IllegalArgumentException("Missing " + name);
return value;
}
public static void main(String[] args) throws Exception {
String ak = requiredEnv("MEITU_OPENAPI_ACCESS_KEY");
String sk = requiredEnv("MEITU_OPENAPI_SECRET_KEY");
String action = System.getenv().getOrDefault("MEITU_OPENAPI_ACTION", "push");
String method;
String url;
String body = "";
if ("push".equals(action)) {
String filename = System.getenv().getOrDefault("MEITU_REQUEST_FILE", "request.json");
body = Files.readString(Path.of(filename), StandardCharsets.UTF_8);
method = "POST";
url = "https://openapi.meitu.com/api/v1/sdk/sync/push";
} else if ("status".equals(action)) {
method = "GET";
url = "https://openapi.meitu.com/api/v1/sdk/status?task_id="
+ URLEncoder.encode(requiredEnv("MEITU_TASK_ID"), StandardCharsets.UTF_8);
} else {
throw new IllegalArgumentException("MEITU_OPENAPI_ACTION must be push or status");
}
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put(Signer.HeaderHost, "openapi.meitu.com");
Map<String, String> signedHeaders = new Signer(ak, sk).sign(url, method, headers, body);
HttpURLConnection connection = (HttpURLConnection) URI.create(url).toURL().openConnection();
try {
connection.setRequestMethod(method);
connection.setConnectTimeout(10000);
connection.setReadTimeout(100000);
connection.setInstanceFollowRedirects(false);
for (Map.Entry<String, String> header : signedHeaders.entrySet()) connection.setRequestProperty(header.getKey(), header.getValue());
if ("POST".equals(method)) {
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
connection.setDoOutput(true);
connection.setFixedLengthStreamingMode(bytes.length);
try (OutputStream output = connection.getOutputStream()) { output.write(bytes); }
}
int status = connection.getResponseCode();
System.out.println("HTTP " + status);
InputStream stream = status >= 400 ? connection.getErrorStream() : connection.getInputStream();
if (stream != null) {
try (InputStream input = stream) { System.out.println(new String(input.readAllBytes(), StandardCharsets.UTF_8)); }
}
if (status < 200 || status >= 300) throw new IllegalStateException("HTTP request failed");
} finally {
connection.disconnect();
}
}
}Request Examples
See Request Parameters for types, required fields, defaults, and enum values. Save a complete request as request.json for the language examples above. Replace image URLs before submitting requests with image inputs.
Minimal Request
{
"task": "image-to-video",
"task_type": "formula",
"init_images": [
{
"url": "https://example.com/input.jpg",
"profile": {
"version": "v1",
"media_profiles": {
"media_data_type": "url"
}
}
}
],
"params": "{\"parameter\":{\"prompt\":\"Slowly move the camera forward while keeping the subject appearance and scene consistent\"}}",
"sync_timeout": 30
}Specify Generation Settings
{
"task": "image-to-video",
"task_type": "formula",
"init_images": [
{
"url": "https://example.com/input.jpg",
"profile": {
"version": "v1",
"media_profiles": {
"media_data_type": "url"
}
}
}
],
"params": "{\"parameter\":{\"prompt\":\"Slow camera push-in with natural subject motion and stable framing\",\"mode\":\"i2v\",\"model\":\"auto\",\"video_duration\":5,\"sound\":\"off\",\"aspect_ratio\":\"16:9\"}}",
"sync_timeout": 30
}First and Last Frame Transition
{
"task": "image-to-video",
"task_type": "formula",
"init_images": [
{
"url": "https://example.com/input.jpg",
"profile": {
"version": "v1",
"media_profiles": {
"media_data_type": "url"
}
}
},
{
"url": "https://example.com/end.jpg",
"profile": {
"version": "v1",
"media_profiles": {
"media_data_type": "url"
}
}
}
],
"params": "{\"parameter\":{\"prompt\":\"Smoothly transition from the first frame to the last frame with natural, continuous motion\",\"mode\":\"flf\",\"model\":\"auto\",\"video_duration\":5}}",
"sync_timeout": 30
}