</>创作 API
API v2管理后台 ↗
文档目录

DEVELOPER DOCUMENTATION

把生成能力接入你的业务

提交一张图片,选择生成模板。通过任务查询或 Webhook 回调,获取处理结果与可下载的图片、视频地址。

BASE URLhttps://zxncdo.com/api/v2
01 获取签名密钥→02 提交任务→03 接收结果

接入方式

图片生成和图生视频使用同一套异步接口。服务提供方在后台「商户管理」为每个调用方创建商户,把独立的 8 位对外商户 ID 和请求签名密钥单独交给对方;签名密钥是后台显示的「请求与回调签名密钥」。调用方在自己的服务器保存密钥,不能放在网页或 App 中。后台内部数据库编号不是对外商户 ID。

调用方获取模板 ID,提交图片和自己的回调地址;接口立即返回任务 ID。生成完成后,本服务主动回调成功或失败状态及图片/视频资源地址。调用方也可以按任务 ID 查询。

用途方法地址
获取可用模板GET/api/v2/templates?kind=video 或 kind=dress
提交生成任务POST/api/v2/generate
查询任务结果GET/api/v2/tasks/{task_id}
查询商户额度GET/api/v2/me
查询资金流水GET/api/v2/ledger?limit=50

传输说明:HMAC 签名验证调用方身份及请求完整性,不会对图片和请求内容加密。请通过上面的 HTTPS 域名调用,以保护传输中的图片、签名密钥相关请求信息和回调地址。

一、请求签名

每个 /api/v2 请求都必须带以下请求头。无需登录、无需 Bearer token:

请求头值
X-Merchant-Id后台「商户管理」左侧「对外商户 ID / X-Merchant-Id」列的随机 8 位十进制数字,例如 12345678;可点击同一行的「复制」。不能使用后台内部编号
X-Timestamp当前 Unix 秒,例如 1791131904;服务端允许与当前时间相差最多 300 秒
X-Nonce每次请求重新生成的随机字符串,长度 16–64,只能含字母、数字、_、-;同一商户不能复用
X-Signature下述 HMAC-SHA256 的 64 位十六进制字符串

已接入的商户也需要从后台重新复制 8 位对外商户 ID,同时更新请求头和待签名内容中的商户 ID;原数据库编号会返回 401。签名密钥不因这次编号迁移而改变。

按以下顺序用换行符 \n 拼接待签名内容,最后一行之后不加换行:

TEXT
大写 HTTP 方法
URL 路径及原始查询字符串
商户 ID
Unix 秒时间戳
随机数
最终请求的 Content-Type 头值(没有则为空字符串)
原始请求体的 SHA256 小写十六进制摘要

例如 GET /api/v2/templates?kind=video,空请求体使用 SHA256(空字节):

TEXT
GET
/api/v2/templates?kind=video
12345678
1791131904
7a4d99cbe0f113ac

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

实际签名是 hex(HMAC-SHA256(密钥的 UTF-8 字节, 待签名内容的 UTF-8 字节)),填入 X-Signature。密钥按后台显示的原字符串使用,不做十六进制解码。URL 不包含协议和域名,查询参数的顺序及编码必须与实际发出的请求完全一致。

上传图片时,先完成 multipart/form-data 序列化,再对最终发送的整个请求体字节求 SHA256,包括 boundary、字段和图片。Content-Type 必须使用最终发送的完整头值,例如 multipart/form-data; boundary=----creation...。不能只对图片文件计算摘要,也不能在签名后再改写 body 或 Content-Type。签名错误、时间戳过期、重复 nonce、商户停用都会返回 401;重复 nonce 记录在数据库中,服务重启后仍有效。图片上传过大返回 413。

二、获取模板

TEXT
GET /api/v2/templates?kind=video
GET /api/v2/templates?kind=dress

只返回已上架模板。kind=video 是图生视频,kind=dress 是图片模板。选择返回的 id 作为生成请求里的 template_id。响应中包含 name、cover_url、price、featured;视频模板还可能有示例 video_url。示例地址不是本次任务的产物。price 的单位是“金”,可以有最多两位小数,例如 1.25;原来整数价格的数值保持不变。

JSON
[{"id":73,"kind":"video","name":"视频模板","cover_url":"https://cdn.example.com/cover.png","video_url":"https://cdn.example.com/example.mp4","price":1.25,"featured":true}]

模板提供默认提示词;调用方也可在提交任务时传 prompt,只覆盖该次任务,不修改模板。视频秒数(5~8 秒)和 GPU 地址仍由模板配置,生成请求不接收 seconds 或机器地址。

三、提交生成任务

TEXT
POST /api/v2/generate
Content-Type: multipart/form-data
表单字段必填说明
template_id是已上架模板 ID,决定生成图片或视频
file是PNG、JPG、WebP、GIF 图片,不超过 20 MB
prompt否本次任务的自定义提示词;不传或只传空白时使用模板默认提示词
seed否整数随机种子;不传由下游服务生成
callback_url否调用方自己的公网 HTTP(S) 回调地址;建议填写

视频模板会把 prompt 原样发给图生视频服务;普通换衣模板会把它填入模板的提示词句式。脱衣模板的下游不支持提示词,传入非空 prompt 会返回 400,不会创建任务或扣费。没有额外的提示词字符数限制,但整个签名请求体(图片、字段和 multipart 边界)不得超过 22 MiB;prompt 的 UTF-8 字节必须包含在请求体摘要中。

成功提交立即返回 HTTP 202:

JSON
{"task_id":137,"status":"queued","cost":1.25}

202 表示任务已创建并扣费,不表示已经生成成功。cost 和模板 price 的单位都是“金”,最多精确到 0.01 金;最终失败会自动退回相同金额。每次 POST 都是新任务;如果请求超时且没有收到任务 ID,不要直接重复提交,以免重复创建和扣费。

参数错误返回 400,无效签名返回 401,余额不足返回 402,模板不存在或已下架返回 404,请求体过大返回 413。这些同步拒绝的请求不会产生任务,也不会回调。

四、查询任务与资源

TEXT
GET /api/v2/tasks/{task_id}

只能查询本商户的任务;其他商户的任务返回 404。status 依次为 queued(排队)、running(生成中)、success(成功)或 failed(失败)。图片任务返回 image_url,视频任务返回 video_url;成功时该地址可直接 GET 下载,失败时为空字符串并在 error 返回原因。

JSON
{"task_id":137,"status":"success","video_url":"https://cdn.example.com/outputs/result.mp4","seed":8197001,"elapsed_ms":123241,"error":""}

查询 GET /api/v2/me 可得到 id、name 和剩余额度 balance;其中 id 是同一个 8 位对外商户 ID,balance 的单位也是“金”且可能带两位小数,响应不会返回签名密钥。排队时间不固定,调用方可每 3–5 秒查询一次,或等待回调。界面等待超时不代表服务端任务已失败。

商户还可以签名查询 GET /api/v2/ledger?limit=50,按时间倒序查看自己的额度流水。limit 默认 50、最多 100;响应的 next_before_id 不为空时,用 GET /api/v2/ledger?limit=50&before_id=该值 取下一页。查询参数必须包含在请求签名中,其他商户的流水不可访问。

JSON
{"merchant_id":12345678,"balance":10.25,"items":[{"id":205,"kind":"refund","delta":1.25,"balance_after":10.25,"creation_id":137,"remark":"生成失败退款","created_at":"2026-10-05T12:00:00Z"}],"next_before_id":null}

delta 为正表示入账、为负表示扣账,金额单位仍是“金”,最多两位小数。kind 为 opening(期初余额)、credit(后台充值)、debit(后台扣减)、charge(任务扣费)、refund(失败退款)。历史商户启用流水时只补一条当时余额的 opening,不会把之前的每笔充值和消费伪造成明细;新交易都会逐笔记录。creation_id 为空表示没有关联任务。对接方应按小数金额处理 price、cost、balance、delta 和 balance_after,不能假设它们永远是整数。

五、完成回调

提交任务时填写 callback_url 后,任务最终成功或失败时,本服务向该地址发送一次 JSON;对方返回 2xx 即视为接收成功。回调请求体与查询任务的响应一致,例如:

TEXT
POST https://your-domain.example/webhooks/creation
Content-Type: application/json
X-Signature: hex(HMAC-SHA256(商户签名密钥, 原始 JSON 请求体))
JSON
{"task_id":137,"status":"success","video_url":"https://cdn.example.com/outputs/result.mp4","seed":8197001,"elapsed_ms":123241,"error":""}

失败时 status=failed、资源地址为空、error 为失败原因。图片任务用 image_url 字段。回调签名只签原始 JSON 请求体,与调用方请求接口时的七行签名格式不同。接收端使用原始 body 字节验签,不要把 JSON 重新序列化后再验签:

PYTHON
import hmac, hashlib
expected = hmac.new(secret.encode('utf-8'), raw_request_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers['X-Signature']):
    raise ValueError('invalid callback signature')

对方应按 task_id 去重,可靠保存结果后在 10 秒内返回 2xx。非 2xx 或超时会在约 5 秒、30 秒、2 分钟后重试,总计最多 4 次;下载视频等耗时工作应异步执行。回调可能早于提交接口响应到达;漏收时用任务查询兜底。当前回调投递在后台进程内,进程重启可能中断投递,因此不能只依赖回调。

六、可运行的请求示例

四种语言都执行同一流程:查询视频模板、上传图片提交任务、查询一次任务状态。先在后台取得商户 ID 与签名密钥,设置 MERCHANT_ID、SIGNING_SECRET、TEMPLATE_ID、IMAGE_PATH;若需要结果回调,再设置 CALLBACK_URL。TEMPLATE_ID 应选模板列表中已上架的视频模板 ID。API_BASE_URL 默认 https://zxncdo.com,测试环境可覆盖。提交后查询一次通常仍是 queued 或 running,最终结果以回调或后续查询为准。

在 macOS/Linux 终端中先设置以下变量,替换成实际值。Windows 环境使用对应的环境变量设置方式:

BASH
export MERCHANT_ID='12345678'
export SIGNING_SECRET='后台发放的实际签名密钥'
export TEMPLATE_ID='73'
export IMAGE_PATH='./input.jpg'
# 可选:export CALLBACK_URL='https://你的域名/webhooks/creation'
# 可选:export CUSTOM_PROMPT='Six English scenes. The camera slowly moves.'

Python 示例

保存为 example.py,运行 python3 example.py。只依赖 Python 标准库。

PYTHON
import hashlib, hmac, json, os, secrets, time, urllib.request
from pathlib import Path

BASE = os.getenv('API_BASE_URL', 'https://zxncdo.com').rstrip('/')
MERCHANT_ID = os.environ['MERCHANT_ID']
SECRET = os.environ['SIGNING_SECRET']

def call(method, path, body=b'', content_type=None):
    timestamp = str(int(time.time()))
    nonce = secrets.token_hex(16)
    digest = hashlib.sha256(body).hexdigest()
    canonical = '\n'.join([method, path, MERCHANT_ID, timestamp, nonce,
                           content_type or '', digest])
    signature = hmac.new(SECRET.encode(), canonical.encode(), hashlib.sha256).hexdigest()
    headers = {
        'X-Merchant-Id': MERCHANT_ID,
        'X-Timestamp': timestamp,
        'X-Nonce': nonce,
        'X-Signature': signature,
    }
    if content_type:
        headers['Content-Type'] = content_type
    request = urllib.request.Request(BASE + path, data=body if method == 'POST' else None,
                                     headers=headers, method=method)
    with urllib.request.urlopen(request, timeout=30) as response:
        return json.load(response)

print(call('GET', '/api/v2/templates?kind=video'))

template_id = os.environ['TEMPLATE_ID']
boundary = '----creation' + secrets.token_hex(12)
image = Path(os.environ['IMAGE_PATH']).read_bytes()
callback_url = os.getenv('CALLBACK_URL')
custom_prompt = os.getenv('CUSTOM_PROMPT')

def form_field(name, value):
    return (f'--{boundary}\r\nContent-Disposition: form-data; name="{name}"\r\n'
            f'\r\n{value}\r\n').encode()

body = form_field('template_id', template_id)
if custom_prompt:
    body += form_field('prompt', custom_prompt)
if callback_url:
    body += form_field('callback_url', callback_url)
body += (f'--{boundary}\r\nContent-Disposition: form-data; name="file"; '
         f'filename="input.jpg"\r\nContent-Type: image/jpeg\r\n\r\n').encode()
body += image + f'\r\n--{boundary}--\r\n'.encode()

result = call('POST', '/api/v2/generate', body,
              f'multipart/form-data; boundary={boundary}')
print(result)  # {'task_id': ..., 'status': 'queued', 'cost': ...}
print(call('GET', f"/api/v2/tasks/{result['task_id']}"))
print(call('GET', '/api/v2/ledger?limit=20'))

Go 示例

保存为 main.go,运行 go run main.go。只依赖 Go 标准库。

GO
package main

import (
	"bytes"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"
)

func required(name string) string {
	value := os.Getenv(name)
	if value == "" { panic("请设置环境变量 " + name) }
	return value
}

func nonce() string {
	var b [16]byte
	if _, err := rand.Read(b[:]); err != nil { panic(err) }
	return hex.EncodeToString(b[:])
}

func call(method, path string, body []byte, contentType string) ([]byte, error) {
	merchantID, secret := required("MERCHANT_ID"), required("SIGNING_SECRET")
	timestamp, random := fmt.Sprint(time.Now().Unix()), nonce()
	digest := sha256.Sum256(body)
	canonical := strings.Join([]string{method, path, merchantID, timestamp, random,
		contentType, hex.EncodeToString(digest[:])}, "\n")
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(canonical))
	base := os.Getenv("API_BASE_URL")
	if base == "" { base = "https://zxncdo.com" }
	req, err := http.NewRequest(method, strings.TrimRight(base, "/")+path, bytes.NewReader(body))
	if err != nil { return nil, err }
	req.Header.Set("X-Merchant-Id", merchantID)
	req.Header.Set("X-Timestamp", timestamp)
	req.Header.Set("X-Nonce", random)
	req.Header.Set("X-Signature", hex.EncodeToString(mac.Sum(nil)))
	if contentType != "" { req.Header.Set("Content-Type", contentType) }
	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil { return nil, err }
	defer resp.Body.Close()
	data, err := io.ReadAll(resp.Body)
	if err != nil { return nil, err }
	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
		return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, data)
	}
	return data, nil
}

func run() error {
	list, err := call("GET", "/api/v2/templates?kind=video", nil, "")
	if err != nil { return err }
	fmt.Println("模板:", string(list))
	var body bytes.Buffer
	form := multipart.NewWriter(&body)
	if err := form.WriteField("template_id", required("TEMPLATE_ID")); err != nil { return err }
	if prompt := os.Getenv("CUSTOM_PROMPT"); prompt != "" {
		if err := form.WriteField("prompt", prompt); err != nil { return err }
	}
	if callback := os.Getenv("CALLBACK_URL"); callback != "" {
		if err := form.WriteField("callback_url", callback); err != nil { return err }
	}
	imagePath := required("IMAGE_PATH")
	image, err := os.Open(imagePath)
	if err != nil { return err }
	defer image.Close()
	file, err := form.CreateFormFile("file", filepath.Base(imagePath))
	if err != nil { return err }
	if _, err := io.Copy(file, image); err != nil { return err }
	if err := form.Close(); err != nil { return err }
	result, err := call("POST", "/api/v2/generate", body.Bytes(), form.FormDataContentType())
	if err != nil { return err }
	fmt.Println("提交:", string(result))
	var task struct { TaskID int64 `json:"task_id"` }
	if err := json.Unmarshal(result, &task); err != nil { return err }
	status, err := call("GET", fmt.Sprintf("/api/v2/tasks/%d", task.TaskID), nil, "")
	if err != nil { return err }
	fmt.Println("任务:", string(status))
	ledger, err := call("GET", "/api/v2/ledger?limit=20", nil, "")
	if err != nil { return err }
	fmt.Println("流水:", string(ledger))
	return nil
}

func main() {
	if err := run(); err != nil { fmt.Fprintln(os.Stderr, err); os.Exit(1) }
}

Java 示例

保存为 CreatorApiExample.java,使用 Java 17 或以上运行 javac CreatorApiExample.java && java CreatorApiExample。只依赖 JDK 标准库。

JAVA
import java.io.ByteArrayOutputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.time.Duration;
import java.time.Instant;
import java.util.HexFormat;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class CreatorApiExample {
    static final String BASE = System.getenv().getOrDefault("API_BASE_URL", "https://zxncdo.com").replaceAll("/+$", "");
    static final HttpClient CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();

    static String required(String name) {
        String value = System.getenv(name);
        if (value == null || value.isEmpty()) throw new IllegalArgumentException("请设置环境变量 " + name);
        return value;
    }

    static String nonce() {
        byte[] bytes = new byte[16];
        new SecureRandom().nextBytes(bytes);
        return HexFormat.of().formatHex(bytes);
    }

    static String call(String method, String path, byte[] body, String contentType) throws Exception {
        String merchantId = required("MERCHANT_ID");
        String timestamp = Long.toString(Instant.now().getEpochSecond());
        String random = nonce();
        String digest = HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(body));
        String canonical = String.join("\n", method, path, merchantId, timestamp, random, contentType, digest);
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(required("SIGNING_SECRET").getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        String signature = HexFormat.of().formatHex(mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8)));
        HttpRequest.Builder request = HttpRequest.newBuilder(URI.create(BASE + path))
            .timeout(Duration.ofSeconds(30))
            .header("X-Merchant-Id", merchantId)
            .header("X-Timestamp", timestamp)
            .header("X-Nonce", random)
            .header("X-Signature", signature);
        if (!contentType.isEmpty()) request.header("Content-Type", contentType);
        request.method(method, method.equals("GET")
            ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofByteArray(body));
        HttpResponse<String> response = CLIENT.send(request.build(), HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() < 200 || response.statusCode() >= 300)
            throw new IllegalStateException("HTTP " + response.statusCode() + ": " + response.body());
        return response.body();
    }

    static void field(ByteArrayOutputStream out, String boundary, String name, String value) throws Exception {
        String text = "--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + name
            + "\"\r\n\r\n" + value + "\r\n";
        out.write(text.getBytes(StandardCharsets.UTF_8));
    }

    public static void main(String[] args) throws Exception {
        System.out.println("模板: " + call("GET", "/api/v2/templates?kind=video", new byte[0], ""));
        String boundary = "----creation" + nonce();
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        field(out, boundary, "template_id", required("TEMPLATE_ID"));
        String prompt = System.getenv("CUSTOM_PROMPT");
        if (prompt != null && !prompt.isEmpty()) field(out, boundary, "prompt", prompt);
        String callback = System.getenv("CALLBACK_URL");
        if (callback != null && !callback.isEmpty()) field(out, boundary, "callback_url", callback);
        out.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\"file\"; "
            + "filename=\"input.png\"\r\nContent-Type: application/octet-stream\r\n\r\n")
            .getBytes(StandardCharsets.UTF_8));
        out.write(Files.readAllBytes(Path.of(required("IMAGE_PATH"))));
        out.write(("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
        String submitted = call("POST", "/api/v2/generate", out.toByteArray(),
            "multipart/form-data; boundary=" + boundary);
        System.out.println("提交: " + submitted);
        Matcher match = Pattern.compile("\"task_id\"\\s*:\\s*(\\d+)").matcher(submitted);
        if (!match.find()) throw new IllegalStateException("响应缺少 task_id");
        System.out.println("任务: " + call("GET", "/api/v2/tasks/" + match.group(1), new byte[0], ""));
        System.out.println("流水: " + call("GET", "/api/v2/ledger?limit=20", new byte[0], ""));
    }
}

PHP 示例

保存为 example.php,使用 PHP 8.1 或以上并启用 cURL 扩展,运行 php example.php。这里发送的是已序列化的原始 multipart 字节,不使用 CURLFile,以保证签名与实际请求体一致。

PHP
<?php
$base = rtrim(getenv('API_BASE_URL') ?: 'https://zxncdo.com', '/');
$merchantId = getenv('MERCHANT_ID') ?: throw new RuntimeException('请设置 MERCHANT_ID');
$secret = getenv('SIGNING_SECRET') ?: throw new RuntimeException('请设置 SIGNING_SECRET');

function callApi(string $method, string $path, string $body = '', string $contentType = ''): array {
    global $base, $merchantId, $secret;
    $timestamp = (string) time();
    $nonce = bin2hex(random_bytes(16));
    $canonical = implode("\n", [
        $method, $path, $merchantId, $timestamp, $nonce, $contentType, hash('sha256', $body)
    ]);
    $signature = hash_hmac('sha256', $canonical, $secret);
    $headers = [
        'X-Merchant-Id: ' . $merchantId,
        'X-Timestamp: ' . $timestamp,
        'X-Nonce: ' . $nonce,
        'X-Signature: ' . $signature,
    ];
    if ($contentType !== '') $headers[] = 'Content-Type: ' . $contentType;
    $curl = curl_init($base . $path);
    curl_setopt_array($curl, [
        CURLOPT_CUSTOMREQUEST => $method,
        CURLOPT_HTTPHEADER => $headers,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT => 30,
    ]);
    if ($method === 'POST') curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
    $response = curl_exec($curl);
    if ($response === false) throw new RuntimeException(curl_error($curl));
    $status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
    unset($curl);
    if ($status < 200 || $status >= 300)
        throw new RuntimeException("HTTP $status: $response");
    return json_decode($response, true, 512, JSON_THROW_ON_ERROR);
}

function formField(string $boundary, string $name, string $value): string {
    return "--$boundary\r\nContent-Disposition: form-data; name=\"$name\"\r\n\r\n$value\r\n";
}

echo '模板: ' . json_encode(callApi('GET', '/api/v2/templates?kind=video')) . PHP_EOL;
$templateId = getenv('TEMPLATE_ID') ?: throw new RuntimeException('请设置 TEMPLATE_ID');
$imagePath = getenv('IMAGE_PATH') ?: throw new RuntimeException('请设置 IMAGE_PATH');
$image = file_get_contents($imagePath);
if ($image === false) throw new RuntimeException('无法读取图片');
$boundary = '----creation' . bin2hex(random_bytes(12));
$body = formField($boundary, 'template_id', $templateId);
$prompt = getenv('CUSTOM_PROMPT');
if ($prompt !== false && $prompt !== '')
    $body .= formField($boundary, 'prompt', $prompt);
$callback = getenv('CALLBACK_URL');
if ($callback !== false && $callback !== '')
    $body .= formField($boundary, 'callback_url', $callback);
$body .= "--$boundary\r\nContent-Disposition: form-data; name=\"file\"; "
    . "filename=\"input.png\"\r\nContent-Type: application/octet-stream\r\n\r\n";
$body .= $image . "\r\n--$boundary--\r\n";
$submitted = callApi('POST', '/api/v2/generate', $body,
    'multipart/form-data; boundary=' . $boundary);
echo '提交: ' . json_encode($submitted) . PHP_EOL;
echo '任务: ' . json_encode(callApi('GET', '/api/v2/tasks/' . $submitted['task_id'])) . PHP_EOL;
echo '流水: ' . json_encode(callApi('GET', '/api/v2/ledger?limit=20')) . PHP_EOL;

如果是图片生成,模板列表改查 kind=dress,提交格式和回调验签相同。

七、上线验收