职场风写真 API 接入文档
版本:V1 | 更新日期:2026-10-29
职场风写真(meituhub_workflow_9486_v1)将输入照片转化为高知职场风写真。提交一张人物照片,可通过“其他要求”补充服装等要求。
本文提供线上 API 的鉴权、请求参数、结果查询,以及 Python、Go、PHP、Java 接入示例。示例中的媒体 URL、凭证和任务 ID 均需替换为实际值。
目录
接入准备
获取 AK 和 SK
-
登录 MeituHub 控制台,进入“账户总览”页面。
-
在“API 密钥”区域,分别点击以下字段旁的“复制”按钮,获取 AK 和 SK:
MEITU_OPENAPI_ACCESS_KEY:Access Key(AK)。MEITU_OPENAPI_SECRET_KEY:Secret Key(SK)。
SK 仅用于服务端签名,请勿放入浏览器代码、移动端、公开仓库或日志。
请求签名
提交和查询请求均需使用 AK/SK 签名。使用官方签名 SDK 对完整 URL、HTTP 方法、请求头和请求体进行签名,并发送 SDK 生成的请求;签名后请勿修改请求体。
签名规则见 开放平台接口签名。
提交任务
POST https://openapi.meitu.com/api/v1/sdk/sync/push
Content-Type: application/json请求参数
下表中的 params.prompt_1 表示 params 字符串解析后的字段路径。将 prompt_1 放在 JSON 对象的顶层,再将该对象序列化为 params 字符串。
| 字段 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| task | string | 是 | 固定值 | 工作流标识,固定为 meituhub_workflow_9486_v1。 |
| task_type | string | 是 | 固定值 | 固定为 cozeflow。 |
| init_images | object[] | 是 | 无 | 输入图片列表,传入一张人物照片,放在 init_images[0]。 |
| init_images[0].url | string | 是 | 无 | 输入图。填写服务端可直接访问的图片 URL;请替换能力页的示例图片。 |
| params | string | 是 | 无 | 工作流参数的 JSON 字符串;不填写其他要求时传 "{}"。 |
| params.prompt_1 | string | 否 | 能力页预填:黑色高领短袖 | 其他要求,自由文本,无枚举限制。如需沿用预填要求,请显式传入该值。 |
| sync_timeout | integer | 否 | 本文示例为 30 | 同步等待时间,单位秒。超时后保存任务 ID 并继续查询。 |
媒体输入
init_images 中的每个元素使用以下结构:
{
"url": "https://example.com/input.jpg",
"profile": {
"version": "v1",
"media_profiles": {
"media_data_type": "url"
}
}
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| url | string | 是 | 服务端可直接访问的图片 URL。 |
| profile.version | string | 是 | 固定为 v1。 |
| profile.media_profiles.media_data_type | string | 是 | 固定为 url。 |
本工作流只有一个图片输入位置:init_images[0] 对应“输入图”。请使用实际人物照片,不要增加参考图位置。
能力参数
其他要求使用 prompt_1 字段。序列化前的对象示例:
{
"prompt_1": "黑色高领短袖"
}请求体中的 params 字段示例:
{
"params": "{\"prompt_1\":\"黑色高领短袖\"}"
}params 的外层类型必须是字符串。prompt_1 是可选的自由文本;不填写其他要求时,可将 params 设为 "{}"。能力页预填值与客户端实际提交值可能不同,如需使用预填要求,请显式传入。
返回结果
返回字段
网关响应可能包含 code、error_code、message、data 包装。包含包装时,先检查外层业务码,再读取 data 中的任务对象;SDK 若直接返回任务对象,则从顶层读取 status、task_id、result 等字段。
| 字段 | 类型 | 说明 |
|---|---|---|
| code | integer,可选 | 网关业务码,0 表示请求处理正常;非零时结合错误信息处理。 |
| error_code | integer,可选 | 网关错误码,0 表示无对应网关错误。 |
| message | string,可选 | 网关提示或错误信息。 |
| request_id / trace_id | string,可选 | 请求追踪标识,排查问题时保留。 |
| data | object,可选 | 任务对象;直接返回任务对象时无该包装。 |
下表中的字段相对于任务对象描述:
| 字段 | 类型 | 说明 |
|---|---|---|
| status | integer | 任务状态,见任务状态表。 |
| task_id | string | 任务 ID,用于后续查询。 |
| progress | number,可选 | 任务进度,通常为 0–1。 |
| result | object / null | 任务结果;未完成或失败时可能为空。 |
| result.id | string,可选 | 结果关联的任务 ID。 |
| result.media_info_list | object[],可选 | 输出图片列表;当前工作流配置的输出为 output_1,对应首张结果图。 |
| result.media_info_list[].media_data | string | 输出图片 URL。 |
| result.media_info_list[].media_profiles | object,可选 | 媒体描述;URL 结果的 media_data_type 为 url。 |
| result.media_info_list[].media_extra | object / null,可选 | 媒体附加信息。 |
| result.parameters | JSON 值,可选 | 能力相关的附加业务数据。 |
| result.code / result.error_code | integer,可选 | 结果中的业务错误码,非零时检查错误信息。 |
| result.msg / result.error_msg | string,可选 | 结果中的提示或错误信息。 |
任务状态
| status | 含义 | 处理方式 |
|---|---|---|
| -1 | 未找到任务 | 检查任务 ID、调用账号和任务有效期。 |
| 0 | 已创建 | 保存任务 ID,继续查询。 |
| 1 | 执行中 | 继续查询。 |
| 2 | 执行失败 | 根据网关和结果中的错误信息处理。 |
| 9 | 同步等待超时 | 保存任务 ID,继续查询。 |
| 10 | 执行成功 | 读取 result.media_info_list。 |
HTTP 200 仅表示 HTTP 请求已完成。请结合外层业务码、任务 status 和结果错误信息判断任务是否成功,不要仅以 progress=1 或存在 task_id 作为成功依据。
成功示例
以下为含网关包装的字段结构示例,任务 ID 和媒体地址为占位值:
{
"code": 0,
"error_code": 0,
"message": "success",
"data": {
"progress": 1,
"result": {
"id": "example-task-id",
"media_info_list": [
{
"media_data": "https://example.com/result.png",
"media_extra": {},
"media_profiles": {
"media_data_type": "url"
}
}
],
"parameters": {}
},
"status": 10,
"task_id": "example-task-id"
}
}含 data 包装时,首张结果图片地址为 data.result.media_info_list[0].media_data;直接返回任务对象时,为 result.media_info_list[0].media_data。请按返回列表处理全部媒体,并及时下载保存结果文件。
同步等待超时示例
{
"code": 0,
"error_code": 0,
"message": "success",
"data": {
"progress": 0,
"result": {
"id": "example-task-id"
},
"status": 9,
"task_id": "example-task-id"
}
}status=9 不代表任务执行失败。使用返回的 task_id 查询后续结果。
失败示例
{
"code": 0,
"error_code": 0,
"message": "success",
"data": {
"progress": 0,
"result": null,
"status": 2,
"task_id": "example-task-id"
}
}外层业务码为 0 时,任务仍可能以 status=2 执行失败。鉴权失败、限流或参数错误时,也可能直接返回非零业务码、非 2xx HTTP 状态,且不包含任务 ID。请保留错误响应和请求追踪标识;通用错误码见 API 错误码。
查询任务
GET https://openapi.meitu.com/api/v1/sdk/status?task_id=example-task-id| 参数 | 位置 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| task_id | Query | string | 是 | 提交任务后返回的任务 ID。 |
使用与提交账号一致的 AK/SK,对包含 task_id 的完整 URL 和 GET 方法签名,请求体为空字符串。查询返回的任务字段与状态含义见“返回结果”。
建议逐步延长查询间隔,例如 2 秒、5 秒、10 秒,并设置业务等待上限。到达等待上限后保留任务 ID,可稍后继续查询。任务信息和媒体链接可能过期,请及时查询和保存结果。
若提交请求发生网络超时且未获得任务 ID,请先核实任务是否已创建,避免直接重复提交。
多语言接入示例
四种语言示例共用 request.json:从“请求示例”复制完整请求体,替换媒体 URL 和其他占位值后保存为该文件。
SDK 准备
| 语言 | 官方接入文档 | 依赖 |
|---|---|---|
| Python | Python SDK | sign_sdk、requests |
| Go | Go SDK | github.com/mtlab/api/signer |
| PHP | PHP SDK | 官方 signer.php、cURL 扩展,PHP 7.3+ |
| Java | Java SDK | 官方签名 SDK,JDK 11+ |
Python 签名包可通过 SDK 下载地址 获取。解压后将 sign_sdk 加入导入路径,并安装 requests:
python -m pip install requests运行配置
提交任务:
export MEITU_OPENAPI_ACTION='push'
export MEITU_REQUEST_FILE='request.json'查询任务时,改为以下配置:
export MEITU_OPENAPI_ACTION='status'
export MEITU_TASK_ID='example-task-id'每次运行仅执行所选操作,并打印完整 HTTP 响应。示例不会自动轮询或重试提交;获取响应后,按“返回结果”中的业务码和任务状态处理。
Python
保存为 client.py,运行 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
保存为 main.go,在已引入签名 SDK 的 Go 模块中运行 go run main.go:
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
将官方 signer.php 放在同一目录,保存为 client.php,运行 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
将官方 SDK 加入 classpath,保存为 Main.java,使用 JDK 11+ 编译运行。request.json 使用 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();
}
}
}请求示例
字段类型、必填项和预填值见请求参数。以下请求均使用一张输入图,params 为 JSON 字符串。
使用能力页预填要求
显式传入“黑色高领短袖”。将以下完整请求保存为 request.json,替换输入图片 URL 后即可用于上面的多语言示例:
{
"task": "meituhub_workflow_9486_v1",
"task_type": "cozeflow",
"init_images": [
{
"url": "https://example.com/input.jpg",
"profile": {
"version": "v1",
"media_profiles": {
"media_data_type": "url"
}
}
}
],
"params": "{\"prompt_1\":\"黑色高领短袖\"}",
"sync_timeout": 30
}自定义其他要求
按需要修改 prompt_1,例如指定衬衫与外套:
{
"task": "meituhub_workflow_9486_v1",
"task_type": "cozeflow",
"init_images": [
{
"url": "https://example.com/input.jpg",
"profile": {
"version": "v1",
"media_profiles": {
"media_data_type": "url"
}
}
}
],
"params": "{\"prompt_1\":\"白色衬衫搭配黑色西装外套\"}",
"sync_timeout": 30
}不填写其他要求
省略可选的 prompt_1,保留字符串类型的 params:
{
"task": "meituhub_workflow_9486_v1",
"task_type": "cozeflow",
"init_images": [
{
"url": "https://example.com/input.jpg",
"profile": {
"version": "v1",
"media_profiles": {
"media_data_type": "url"
}
}
}
],
"params": "{}",
"sync_timeout": 30
}