文档目录
DEVELOPER DOCUMENTATION
把生成能力接入你的业务
提交一张图片,选择生成模板。通过任务查询或 Webhook 回调,获取处理结果与可下载的图片、视频地址。
https://zxncdo.com/api/v2接入方式
图片生成和图生视频使用同一套异步接口。服务提供方在后台「商户管理」为每个调用方创建商户,把独立的 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 拼接待签名内容,最后一行之后不加换行:
大写 HTTP 方法
URL 路径及原始查询字符串
商户 ID
Unix 秒时间戳
随机数
最终请求的 Content-Type 头值(没有则为空字符串)
原始请求体的 SHA256 小写十六进制摘要例如 GET /api/v2/templates?kind=video,空请求体使用 SHA256(空字节):
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。
二、获取模板
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;原来整数价格的数值保持不变。
[{"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 或机器地址。
三、提交生成任务
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:
{"task_id":137,"status":"queued","cost":1.25}202 表示任务已创建并扣费,不表示已经生成成功。cost 和模板 price 的单位都是“金”,最多精确到 0.01 金;最终失败会自动退回相同金额。每次 POST 都是新任务;如果请求超时且没有收到任务 ID,不要直接重复提交,以免重复创建和扣费。
参数错误返回 400,无效签名返回 401,余额不足返回 402,模板不存在或已下架返回 404,请求体过大返回 413。这些同步拒绝的请求不会产生任务,也不会回调。
四、查询任务与资源
GET /api/v2/tasks/{task_id}只能查询本商户的任务;其他商户的任务返回 404。status 依次为 queued(排队)、running(生成中)、success(成功)或 failed(失败)。图片任务返回 image_url,视频任务返回 video_url;成功时该地址可直接 GET 下载,失败时为空字符串并在 error 返回原因。
{"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=该值 取下一页。查询参数必须包含在请求签名中,其他商户的流水不可访问。
{"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 即视为接收成功。回调请求体与查询任务的响应一致,例如:
POST https://your-domain.example/webhooks/creation
Content-Type: application/json
X-Signature: hex(HMAC-SHA256(商户签名密钥, 原始 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 重新序列化后再验签:
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 环境使用对应的环境变量设置方式:
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 标准库。
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 标准库。
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 标准库。
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
$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,提交格式和回调验签相同。
七、上线验收
- 用正确密钥签名获取模板成功;错误签名、过期时间戳、重复 nonce 返回
401。 - 修改签名后的查询参数或上传内容,服务器拒绝,不创建任务、不扣额度。
- 视频成功时回调包含可下载的
video_url,图片成功时包含image_url。 - 失败时收到
failed回调,资源地址为空,扣除的额度已退回。 - 回调响应先返回 500、再恢复 200 时,会重试;接收端按
task_id幂等处理。 - 密钥轮换后旧签名立即失效,需把新密钥同步到调用方及回调验签配置。