鸡鸡出没APIDEVELOPER DOCUMENTATION

开始接入

鉴权与 Base URL

先确认接口地址和 KEY,再验证分组权限。

  1. 在API 密钥页创建 KEY,选择对应的文本、图片或视频分组。
  2. 把 KEY 存在服务端环境变量 XCM_API_KEY;不要把它嵌进网页 JavaScript 或移动端安装包。
  3. 用同一 KEY 请求模型目录;使用返回的完整模型 ID。
配置项填写内容适用方式
Originhttps://xcmapi.org直接拼接完整路径,如 /v1/models
Base URLhttps://xcmapi.org/v1OpenAI 兼容 SDK,SDK 会追加 /responses 等路径
鉴权Authorization: Bearer $XCM_API_KEYOpenAI 风格接口;Gemini 见原生协议章节
请求类型Content-Type: application/jsonJSON 请求;上传文件时采用对应 multipart 格式
Shell 环境变量
export XCM_API_KEY="<YOUR_API_KEY>"

export XCM_MODEL="<MODEL_ID>"

export XCM_API_ORIGIN="https://xcmapi.org"
Windows PowerShell
$env:XCM_API_KEY = "<YOUR_API_KEY>"

$env:XCM_MODEL = "<MODEL_ID>"

$env:XCM_API_ORIGIN = "https://xcmapi.org"

开始接入

创建和管理 KEY

KEY 与分组权限一起决定可调用模型。

1

访问 鸡出没API 控制台 并登录。

2

进入“API 密钥”,点击“创建密钥”,按需要设置名称、额度和可用分组。

3

创建后立即复制 Key。页面不会再次显示完整 Key,遗失后请撤销旧 Key 并重新创建。

安全提示不要把完整 API Key 发给他人,也不要写进前端网页、截图、公共仓库或日志。怀疑泄露时,请立即在API 密钥中撤销。

开始接入

模型与分组

模型目录由当前 KEY 的分组权限决定,不同 KEY 的返回列表可能不同。

读取可用模型
curl --fail-with-body --max-time 30 "$XCM_API_ORIGIN/v1/models" \
  -H "Authorization: Bearer $XCM_API_KEY"

取返回的 data[].id 填入 model。模型名称中的中文、数字、标点和大小写均应保留。模型在目录里出现不等于所有多模态接口均已开放,还需检查对应渠道的能力说明。

使用目标选择分组验证内容
文本或代码支持该模型的文本分组模型目录、协议、流式响应
图片生成明确支持生图的分组模型、尺寸、图片输出格式
H3 视频视频综合分组-全球顶尖视频模型-渠道版模型 h3;独立请求参数、按秒价格与创建/查询/下载权限
混合视频视频综合分组-全球顶尖视频模型-渠道版原 10 款按条型号;完整模型 ID、参数与实际验收范围见视频章节

开始接入

完整路径速查

以下为本文覆盖的现有接口。未列出的路径不应按名称自行推断。

用途地址说明
统一 API 地址https://api.tcvps.cn/v1填写到支持 OpenAI 兼容格式的客户端。
模型目录GET /v1/models查看当前 Key 可用的模型。
对话接口POST /v1/chat/completions兼容 Chat Completions 请求格式。
Responses 接口POST /v1/responses兼容 OpenAI Responses 请求格式。
图片接口POST /v1/images/generations文生图;结果可能返回 URL 或 Base64。
H3 创建任务POST /v1/videos模型固定为 h3,异步创建视频任务。
H3 查询任务GET /v1/videos/<REQUEST_ID>轮询状态,完成后读取视频地址。
H3 下载视频GET /v1/videos/<REQUEST_ID>/content任务完成后下载 MP4 内容。

开始接入

基础请求示例

用于最小连通检查;请选择当前 KEY 可用的模型。

查看可用模型

curl
curl https://api.tcvps.cn/v1/models \
  -H "Authorization: Bearer $XCM_API_KEY"

发送一次对话请求

curl
curl https://api.tcvps.cn/v1/chat/completions \
  -H "Authorization: Bearer $XCM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{

    "model": "你的模型名",

    "messages": [{"role": "user", "content": "你好"}]

  }'

Python

python
import os
from openai import OpenAI



client = OpenAI(api_key=os.environ["XCM_API_KEY"], base_url="https://api.tcvps.cn/v1")

response = client.chat.completions.create(

    model="你的模型名",

    messages=[{"role": "user", "content": "你好"}],

)

print(response.choices[0].message.content)

文本 API 对接

文本 API

Chat Completions 与 Responses 是两套请求结构,按你的客户端协议选择。

接口请求内容输出位置
POST /v1/chat/completionsmessages 数组choices[].message / delta
POST /v1/responsesinput 字符串或消息 / 工具项output 数组或 Responses SSE 事件
Chat Completions
curl "https://xcmapi.org/v1/chat/completions" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{

    "model": "<MODEL_ID>",

    "messages": [{"role": "user", "content": "Hello"}],

    "stream": false

  }'
Responses
curl "https://xcmapi.org/v1/responses" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{

    "model": "<MODEL_ID>",

    "input": "Hello"

  }'
Responses · Python 标准库
import json, os, urllib.request

origin = os.environ.get("XCM_API_ORIGIN", "https://xcmapi.org").rstrip("/")

payload = {"model": os.environ["XCM_MODEL"], "input": "Reply OK", "stream": False}

request = urllib.request.Request(origin + "/v1/responses",

    data=json.dumps(payload).encode(), method="POST",

    headers={"Authorization": "Bearer " + os.environ["XCM_API_KEY"],

             "Content-Type": "application/json"})

with urllib.request.urlopen(request, timeout=90) as response:

    result = json.load(response)

for item in result.get("output", []):

    if item.get("type") == "message":

        for content in item.get("content", []):

            if content.get("type") == "output_text":

                print(content["text"])

文本 API 对接

流式响应与工具结果

按完整 SSE 事件处理增量数据,并把工具结果与对应 call_id 配对。

POST/v1/responsesstream: true
curl · 不缓冲输出
curl -N --fail-with-body --max-time 120 "$XCM_API_ORIGIN/v1/responses"   -H "Authorization: Bearer $XCM_API_KEY"   -H "Content-Type: application/json"   -d '{"model":"<MODEL_ID>","input":"Reply OK","stream":true}'
处理阶段客户端动作
分片到达TCP 数据块不等于完整 JSON;按空行分割 SSE,合并同一事件的数据行后解析
文本增量Chat 使用 choices[].delta;Responses 使用 response.output_text.delta
工具调用收齐工具参数,再执行本地工具;保留完整工具项与 call_id
回传结果function_call 对应 function_call_output;custom_tool_call 对应 custom_tool_call_output
终态检查完成、失败或不完整状态;HTTP 200 或连接关闭都不能单独证明任务成功
Responses 工具结果结构(替换真实 call_id)
{

  "model": "<MODEL_ID>",

  "input": [

    {"role": "user", "content": "查询测试数据"},

    {"type":"function_call","call_id":"call_example","name":"lookup","arguments":"{\"id\":1}"},

    {"type":"function_call_output","call_id":"call_example","output":"{\"result\":\"ok\"}"}

  ]

}

图文生成对接

图片生成与编辑

图片 API 使用支持生图的 KEY 分组,上传和生成的请求格式不同。

图片结果通常位于响应的 data[].b64_json 或 data[].url 字段。图片模型、尺寸和可用能力以模型目录为准。

Images Generations
curl "https://xcmapi.org/v1/images/generations" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{

    "model": "<IMAGE_MODEL_ID>",

    "prompt": "A clean product photo",

    "size": "1024x1024",

    "n": 1

  }'
Images Edits
curl "https://xcmapi.org/v1/images/edits" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -F "model=<IMAGE_MODEL_ID>" \
  -F "prompt=Remove the background" \
  -F "image[]=@input.png"

文本 API 对接

Messages / Gemini

只有具备对应能力的模型与 KEY 分组才能使用原生协议入口。

Claude 系列

Anthropic Messages
curl "https://xcmapi.org/v1/messages" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{

    "model": "<MODEL_ID>",

    "max_tokens": 1024,

    "messages": [{"role": "user", "content": "Hello"}]

  }'

Gemini 系列

Gemini generateContent
curl "https://xcmapi.org/v1beta/models/<MODEL_ID>:generateContent" \
  -H "x-goog-api-key: <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{

    "contents": [{"parts": [{"text": "Hello"}]}]

  }'

视频 API 对接

H3 视频对接

一次 POST 创建一个视频任务。创建超时后不要自动重复提交。

创建任务

H3 使用 OpenAI 风格的异步视频接口。模型名称固定为 h3;创建任务后保存返回的任务 ID,每 3 秒左右查询一次,状态完成后读取视频地址或通过 content 接口下载。

已配置分辨率阶梯本站价格(元/秒)说明
480p0.02价格配置已同步;本轮未付费验证出片
720p0.08价格配置已同步;本轮未付费验证出片
1080p0.40价格配置已同步;本轮未付费验证出片
调用前确认先请求 GET /v1/models,确认当前 Key 的模型列表包含 h3。创建任务会产生真实费用,请勿在网络等待时自动重复提交;停止轮询或关闭页面不会取消已提交的服务端任务。
步骤方法与路径说明
创建任务POST /v1/videos
或 POST /v1/videos/generations
提交模型、提示词、时长、画幅、分辨率和可选媒体引用。
查询状态GET /v1/videos/<REQUEST_ID>建议约每 3 秒查询一次,最长等待 10 分钟。
下载视频GET /v1/videos/<REQUEST_ID>/content任务完成后下载;也可读取状态响应中的视频 URL。
1. H3 文生视频
curl "https://xcmapi.org/v1/videos" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{

    "model": "h3",

    "prompt": "一只白色鹈鹕在海边骑自行车,固定镜头,动作自然,画面清晰",

    "duration": 6,

    "aspect_ratio": "16:9",

    "resolution": "480p"

  }'

创建成功后,从响应顶层或 data 对象中读取 request_id、id 或 task_id,后续统一作为 REQUEST_ID 使用。

查询与下载

GET/v1/videos/{task_id}
查询与下载
curl "https://xcmapi.org/v1/videos/<REQUEST_ID>" \
  -H "Authorization: Bearer <YOUR_API_KEY>"



curl -L "https://xcmapi.org/v1/videos/<REQUEST_ID>/content" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -o h3-output.mp4
状态含义处理方式
pending / queued任务已创建,等待执行继续轮询,不要重复创建。
in_progress正在生成约 3 秒后再次查询。
done / completed / succeeded / success生成完成读取 video.url、video_url、顶层 url,或请求 content。
failed / error生成失败记录任务 ID 和错误信息后联系管理员。
首尾帧与多素材
H3 高级能力以下示例使用 H3 1.5 线路的公开请求字段。图片统一使用 {"url":"<IMAGE_URL>"};音频使用 reference_audios 中的预设 voice_id。是否开放首尾帧、多图或音频能力,以当前 Key 的模型目录和分组权限为准。
图片地址请替换示例里的 <IMAGE_URL_n> 只是占位符,不是本站地址。请替换成你自己的、无需登录即可访问的公网 HTTPS 图片地址;不要填写 localhost、127.0.0.1、局域网 IP 或只在本机浏览器中可打开的地址。

首尾帧:固定开始和结束画面

image 固定首帧,last_frame 固定尾帧;只传 last_frame 也可以让模型自动生成开场并落到指定尾帧。提示词可省略,但建议用来描述两帧之间的运动。

H3 首尾帧
curl "https://xcmapi.org/v1/videos/generations" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{

    "model": "h3",

    "prompt": "镜头平滑向前推进,人物从画面左侧走到右侧",

    "image": {"url": "<IMAGE_URL_FIRST>"},

    "last_frame": {"url": "<IMAGE_URL_LAST>"},

    "duration": 8,

    "aspect_ratio": "16:9",

    "resolution": "720p"

  }'

多图参考:同时引用多个主体

把人物、服装、道具或场景分别放入 reference_images,并在提示词中用 <IMAGE_1>、<IMAGE_2> 等标签指代。H3 1.5 单次最多 7 张参考图,超过上限会返回 400。

H3 多图参考
curl "https://xcmapi.org/v1/videos/generations" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{

    "model": "h3",

    "prompt": "<IMAGE_1> 中的人物穿着 <IMAGE_2> 中的衣服,站在 <IMAGE_3> 的场景里,慢慢转身",

    "reference_images": [

      {"url": "<IMAGE_URL_1>"},

      {"url": "<IMAGE_URL_2>"},

      {"url": "<IMAGE_URL_3>"}

    ],

    "duration": 8,

    "aspect_ratio": "9:16",

    "resolution": "720p"

  }'

多图多音频:多角色分别绑定声音

使用 reference_images 和 reference_audios 组合多个角色与声音,在提示词中用 <AUDIO_0>、<AUDIO_1> 绑定说话角色。公开接口的音频示例使用预设声音;自有音频文件需要当前渠道账号单独开通。

H3 多图多音频
curl "https://xcmapi.org/v1/videos/generations" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{

    "model": "h3",

    "prompt": "<IMAGE_1> 中的人物使用 <AUDIO_0> 的声音介绍产品,<IMAGE_2> 中的人物使用 <AUDIO_1> 的声音回应",

    "reference_images": [

      {"url": "<IMAGE_URL_1>"},

      {"url": "<IMAGE_URL_2>"}

    ],

    "reference_audios": [

      {"voice_id": "eve"},

      {"voice_id": "leo"}

    ],

    "duration": 8,

    "aspect_ratio": "16:9",

    "resolution": "720p"

  }'

自动对口型:用参考声音驱动人物说话

当前 H3 公开请求不需要额外传 lip_sync 字段。给人物图片配置 reference_audios,并在提示词中标记对应的 <AUDIO_0>,模型会按声音生成说话动作和口型;最终效果仍取决于人物画面、提示词和当前分组能力。

H3 自动对口型
curl "https://xcmapi.org/v1/videos/generations" \
  -H "Authorization: Bearer <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{

    "model": "h3",

    "prompt": "<IMAGE_1> 中的人物正面看向镜头,用 <AUDIO_0> 的声音自然说话,嘴型与语音同步,保持面部身份一致",

    "reference_images": [

      {"url": "<IMAGE_URL_1>"}

    ],

    "reference_audios": [

      {"voice_id": "eve"}

    ],

    "duration": 8,

    "aspect_ratio": "9:16",

    "resolution": "720p"

  }'
字段可用值说明
modelh3必须使用当前 Key 模型目录中返回的名称。
prompt可选或非空字符串描述主体、动作、镜头和画面风格;使用多图/多音频时用 <IMAGE_n>、<AUDIO_n> 指代输入。
duration1–15 秒不传时按 8 秒处理;在线测试默认使用 6 秒。
aspect_ratio16:9、9:16、1:1 等视频画幅;首尾帧和参考图尽量使用与素材接近的比例。
resolution480p、720p、1080p参考图/音频模式通常最高 720p;实际以当前分组配置为准。
image{"url":"..."}固定首帧或进行单图生视频。
last_frame{"url":"..."}固定尾帧;只传它时模型自动生成开场。
reference_images对象数组多图参考,H3 1.5 单次最多 7 张。
reference_audios{"voice_id":"..."} 数组多音频/对口型;公开预设声音最多 3 个,自有音频需单独开通。
Python / Node 完整代码
命令
# Choose Python OR Node.js, not both for the same intended task.

python h3_client.py --submit

# Resume later without another creation:

python h3_client.py --task-id <TASK_ID>



# Alternative Node.js implementation:

node h3_client.mjs --submit

node h3_client.mjs --task-id <TASK_ID>
Python · 标准库 完整示例
Python · 标准库
"""H3 example: POST once, persist task ID, resume polling without resubmission.

Environment: XCM_API_KEY, optionally XCM_API_ORIGIN (HTTPS origin, no /v1).

Usage: python h3_client.py --submit  OR  python h3_client.py --task-id TASK_ID

Submitting generates a billable video. Polling success is not a billing guarantee.

"""

import argparse

import json

import os

from pathlib import Path

import time

import urllib.error

import urllib.parse

import urllib.request

class NoRedirect(urllib.request.HTTPRedirectHandler):

    def redirect_request(self, req, fp, code, msg, headers, newurl):

        return None  # Never forward the API key to a redirected host.

def request(method, path, payload=None):

    origin = os.environ.get('XCM_API_ORIGIN', 'https://xcmapi.org').rstrip('/')

    parts = urllib.parse.urlsplit(origin)

    if parts.scheme != 'https' or not parts.netloc or parts.path or parts.query or parts.fragment or parts.username:

        raise ValueError('XCM_API_ORIGIN must be an HTTPS origin without /v1')

    key = os.environ['XCM_API_KEY']

    req = urllib.request.Request(origin + path, method=method,

        headers={'Authorization': 'Bearer ' + key, 'Content-Type': 'application/json'},

        data=json.dumps(payload).encode() if payload is not None else None)

    try:

        with urllib.request.build_opener(NoRedirect).open(req, timeout=30) as response:

            data = json.load(response)

            return response.status, data, None

    except urllib.error.HTTPError as error:

        retry = error.headers.get('Retry-After', '')

        retry_seconds = min(15, max(3, int(retry))) if retry.isdigit() else None

        # Do not print a raw body that might contain user content or credentials.

        return error.code, {}, retry_seconds

def unwrap(data):

    return data['data'] if isinstance(data.get('data'), dict) else data

def poll(task_id, attempts=120):

    path = '/v1/videos/' + urllib.parse.quote(task_id, safe='')

    for _ in range(attempts):

        try:

            status, response, retry = request('GET', path)

        except (TimeoutError, urllib.error.URLError):

            time.sleep(5)

            continue  # Retrying GET does not create another video.

        if status in (429, 502, 503, 504):

            time.sleep(retry or 5)

            continue

        if status != 200:

            raise RuntimeError(f'Query HTTP {status}; keep task ID and check the documentation')

        data = unwrap(response)

        state = data.get('status')

        if state in ('failed', 'error', 'cancelled', 'canceled', 'expired'):

            raise RuntimeError('Task ended without success; keep task ID for support')

        if state in ('done', 'completed', 'succeeded', 'success'):

            return data

        if state not in ('pending', 'queued', 'in_progress', 'processing', 'running'):

            raise RuntimeError('Unrecognized task status; inspect the response privately')

        time.sleep(5)

    raise TimeoutError('Polling stopped. Resume using --task-id; do not resubmit the POST')

def download(ident, path):
    """Fetch /content, following at most five HTTPS redirects/JSON links."""
    import re
    from urllib import request as http, error as errors, parse as urls
    if not isinstance(ident, str) or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}', ident):
        raise ValueError('Invalid task ID')
    if not path.is_absolute():
        raise ValueError('Choose an explicit absolute output path')
    if path.exists():
        raise FileExistsError('Output exists; choose a new path')

    def checked_url(value, base=None):
        if not isinstance(value, str) or not value or any(ord(c) <= 32 or ord(c) == 127 for c in value) or '\\' in value:
            raise ValueError('Invalid download URL')
        target = urls.urljoin(base, value) if base else value
        p = urls.urlsplit(target)
        if p.scheme != 'https' or not p.hostname or p.username is not None or p.password is not None or p.fragment or p.port == 0:
            raise ValueError('Download URL must be HTTPS without credentials or fragment')
        return target, (p.hostname.lower(), p.port or 443)

    origin = os.environ.get('XCM_API_ORIGIN', 'https://xcmapi.org').rstrip('/')
    origin, auth_origin = checked_url(origin)
    parsed = urls.urlsplit(origin)
    if parsed.path or parsed.query:
        raise ValueError('Use an HTTPS origin without /v1')
    key = os.environ.get('XCM_API_KEY', '')
    if not key.strip() or '\n' in key or '\r' in key:
        raise ValueError('Set XCM_API_KEY in the server environment')
    url = origin + '/v1/videos/' + urls.quote(ident, safe='') + '/content'
    opener = http.build_opener(NoRedirect)
    send_auth = True
    hops = 0
    limit = 250 * 1024 * 1024
    part = path.with_name(path.name + '.partial')
    created = False
    try:
        while True:
            url, current_origin = checked_url(url)
            send_auth = send_auth and current_origin == auth_origin
            headers = {'Accept': 'video/mp4, application/octet-stream, application/json'}
            if send_auth:
                headers['Authorization'] = 'Bearer ' + key
            req = http.Request(url, headers=headers)
            try:
                response = opener.open(req, timeout=90)
            except errors.HTTPError as exc:
                response = exc
            with response:
                kind = response.headers.get('Content-Type', '').split(';')[0].strip().lower()
                if response.status in (301, 302, 303, 307, 308):
                    target = response.headers.get('Location')
                elif response.status != 200:
                    raise RuntimeError(f'Download HTTP {response.status}; retain task ID')
                elif kind == 'application/json' or kind.endswith('+json'):
                    raw = response.read(1024 * 1024 + 1)
                    if len(raw) > 1024 * 1024:
                        raise RuntimeError('Download JSON exceeds 1MiB')
                    try:
                        data = json.loads(raw)
                    except (ValueError, UnicodeError):
                        raise RuntimeError('Invalid download JSON') from None
                    if isinstance(data, dict) and isinstance(data.get('data'), dict):
                        data = data['data']
                    if not isinstance(data, dict):
                        raise RuntimeError('Expected a download URL object')
                    video = data.get('video')
                    results = data.get('result_urls')
                    target = (data.get('url') or data.get('download_url') or data.get('video_url') or data.get('content_url')
                              or (video.get('url') if isinstance(video, dict) else None)
                              or (results[0] if isinstance(results, list) and results else None))
                else:
                    if kind not in ('video/mp4', 'application/octet-stream'):
                        raise RuntimeError('Not an MP4 download response')
                    length = response.headers.get('Content-Length')
                    if length is not None and (not length.isdigit() or not 0 < int(length) <= limit):
                        raise RuntimeError('Invalid or excessive media length')
                    first = response.read(4096)
                    if len(first) < 12 or first[4:8] != b'ftyp':
                        raise RuntimeError('Response is not an MP4')
                    size = len(first)
                    with part.open('xb') as stream:
                        created = True
                        stream.write(first)
                        while True:
                            chunk = response.read(1024 * 1024)
                            if not chunk:
                                break
                            size += len(chunk)
                            if size > limit:
                                raise RuntimeError('Media exceeds the 250MiB client limit')
                            stream.write(chunk)
                    if length is not None and size != int(length):
                        raise RuntimeError('Incomplete media response; retain task ID')
                    break
                if hops >= 5:
                    raise RuntimeError('Too many download redirects/JSON links (maximum 5)')
                url, _ = checked_url(target, url)
                hops += 1
        # Publish exclusively: a concurrent download must not overwrite this file.
        for attempt in range(6):
            try:
                os.link(part, path)
                break
            except PermissionError:
                if attempt == 5:
                    raise
                time.sleep(.1)
        return size
    finally:
        if created:
            part.unlink(missing_ok=True)

def main():

    parser = argparse.ArgumentParser(description=__doc__)

    action = parser.add_mutually_exclusive_group(required=True)

    action.add_argument('--submit', action='store_true')

    action.add_argument('--task-id')

    parser.add_argument('--output', type=Path, default=Path.cwd() / 'h3-output.mp4')

    args = parser.parse_args()

    task_id = args.task_id

    if args.submit:

        status, response, _ = request('POST', '/v1/videos', {

            'model': 'h3', 'prompt': 'A cat walking slowly on a sunny beach',

            'duration': 6, 'resolution': '480p', 'aspect_ratio': '16:9',

        })

        if status not in (200, 201, 202):

            raise RuntimeError(f'Create HTTP {status}; do not automatically retry POST')

        data = unwrap(response)

        task_id = data.get('request_id') or data.get('id') or data.get('task_id')

        if not isinstance(task_id, str) or not task_id:

            raise RuntimeError('No task ID found; do not automatically resubmit')

        # Contains only your task ID; use a private application database in production.

        Path('h3-last-task.json').write_text(json.dumps({'task_id': task_id}), encoding='utf-8')

    import re
    if not isinstance(task_id, str) or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}', task_id):
        raise ValueError('Invalid task ID; do not resubmit')

    print('Task ID:', task_id)

    poll(task_id)

    size = download(task_id, args.output)
    print('Downloaded:', args.output, 'bytes:', size)

if __name__ == '__main__':

    main()
Node.js 20+ 完整示例
Node.js 20+
// Node.js 20+. XCM_API_KEY is read only from the environment.

// node h3_client.mjs --submit   OR   node h3_client.mjs --task-id TASK_ID

import {writeFile} from 'node:fs/promises';

const origin = (process.env.XCM_API_ORIGIN || 'https://xcmapi.org').replace(/\/$/, '');

const endpoint = new URL(origin);

if (endpoint.protocol !== 'https:' || endpoint.pathname !== '/' || endpoint.search || endpoint.hash || endpoint.username || endpoint.password)

  throw new Error('XCM_API_ORIGIN must be an HTTPS origin without /v1');

if (!process.env.XCM_API_KEY) throw new Error('Set XCM_API_KEY');

const delay = ms => new Promise(resolve => setTimeout(resolve, ms));

async function request(method, path, body) {

  return fetch(origin + path, {method, redirect:'error', signal:AbortSignal.timeout(30000),

    headers:{Authorization:`Bearer ${process.env.XCM_API_KEY}`, 'Content-Type':'application/json'},

    body:body === undefined ? undefined : JSON.stringify(body)});

}

async function download(taskId, output='h3-output.mp4') {
  if(typeof taskId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}$/.test(taskId))
    throw new Error('Invalid task ID');
  function checkedURL(value, base) {
    if(typeof value !== 'string' || !value || /[\s\\\x00-\x1f\x7f]/.test(value)) throw new Error('Invalid download URL');
    const u=new URL(value,base);
    if(u.protocol!=='https:' || !u.hostname || u.username || u.password || u.hash || u.port==='0')
      throw new Error('Download URL must be HTTPS without credentials or fragment');
    return u;
  }
  async function readBounded(response, limit) {
    const chunks=[]; let size=0;
    if(!response.body) throw new Error('Missing download body');
    for await(const chunk of response.body) {
      size+=chunk.byteLength;
      if(size>limit) throw new Error('Download body exceeds its limit');
      chunks.push(Buffer.from(chunk));
    }
    return Buffer.concat(chunks,size);
  }
  let url=checkedURL(origin+'/v1/videos/'+encodeURIComponent(taskId)+'/content');
  let sendAuth=true, hops=0;
  const key=process.env.XCM_API_KEY;
  if(!key || !key.trim() || /[\r\n]/.test(key)) throw new Error('Set XCM_API_KEY');
  while(true) {
    sendAuth=sendAuth && url.origin===endpoint.origin;
    const headers={Accept:'video/mp4, application/octet-stream, application/json'};
    if(sendAuth) headers.Authorization='Bearer '+key;
    const response=await fetch(url.href,{method:'GET',redirect:'manual',headers,signal:AbortSignal.timeout(90000)});
    const kind=(response.headers.get('content-type') || '').split(';')[0].trim().toLowerCase();
    let target;
    if([301,302,303,307,308].includes(response.status)) {
      target=response.headers.get('location');
      await response.body?.cancel();
    } else if(response.status!==200) {
      await response.body?.cancel();
      throw new Error('Download HTTP '+response.status+'; retain task ID');
    } else if(kind==='application/json' || kind.endsWith('+json')) {
      let data=JSON.parse((await readBounded(response,1024*1024)).toString('utf8'));
      if(data && typeof data.data==='object' && data.data!==null && !Array.isArray(data.data)) data=data.data;
      if(!data || typeof data!=='object' || Array.isArray(data)) throw new Error('Expected a download URL object');
      target=data.url || data.download_url || data.video_url || data.content_url || data.video?.url ||
        (Array.isArray(data.result_urls)?data.result_urls[0]:undefined);
    } else {
      if(!['video/mp4','application/octet-stream'].includes(kind)) {
        await response.body?.cancel(); throw new Error('Not an MP4 download response');
      }
      const limit=250*1024*1024, length=response.headers.get('content-length');
      if(length!==null && (!/^\d+$/.test(length) || Number(length)<=0 || Number(length)>limit)) {
        await response.body?.cancel(); throw new Error('Invalid or excessive media length');
      }
      const media=await readBounded(response,limit);
      if(media.length<12 || media.toString('ascii',4,8)!=='ftyp') throw new Error('Response is not an MP4');
      if(length!==null && media.length!==Number(length)) throw new Error('Incomplete media response');
      await writeFile(output,media,{flag:'wx',mode:0o600});
      return media.length;
    }
    if(hops>=5) throw new Error('Too many download redirects/JSON links (maximum 5)');
    url=checkedURL(target,url);
    hops++;
  }
}

const args=process.argv.slice(2);

let taskId;

if(args.length === 2 && args[0] === '--task-id') taskId=args[1];

else if(args.length === 1 && args[0] === '--submit') {

  // One POST only. Do not retry creation automatically after a network timeout.

  const response=await request('POST','/v1/videos',{model:'h3',prompt:'A cat walking slowly on a sunny beach',duration:6,resolution:'480p',aspect_ratio:'16:9'});

  if(!response.ok) throw new Error(`Create HTTP ${response.status}; do not auto-retry POST`);

  const result=await response.json(), data=result.data ?? result;

  taskId=data.request_id ?? data.id ?? data.task_id;

  if(typeof taskId !== 'string' || !taskId) throw new Error('Missing task ID; do not resubmit');

  await writeFile('h3-last-task.json',JSON.stringify({task_id:taskId}),{mode:0o600});

} else throw new Error('Use --submit OR --task-id TASK_ID');

if(typeof taskId !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}$/.test(taskId)) throw new Error('Invalid task ID; do not resubmit');

console.log('Task ID:',taskId);

const taskPath='/v1/videos/'+encodeURIComponent(taskId);

let completed=false;

for(let attempt=0;attempt<120;attempt++) {

  let response;

  try {response=await request('GET',taskPath);} catch {await delay(5000);continue;}

  if([429,502,503,504].includes(response.status)){

    const retry=Number(response.headers.get('retry-after'));

    await delay((Number.isFinite(retry)&&retry>0?Math.min(15,Math.max(3,retry)):5)*1000);continue;

  }

  if(!response.ok) throw new Error(`Query HTTP ${response.status}; retain task ID`);

  const result=await response.json(),data=result.data ?? result,state=data.status;

  if(['failed','error','cancelled','canceled','expired'].includes(state))throw new Error('Task failed; retain task ID');

  if(['done','completed','succeeded','success'].includes(state)){completed=true;break;}

  if(!['pending','queued','in_progress','processing','running'].includes(state))throw new Error('Unknown task status');

  await delay(5000);

}

if(!completed)throw new Error('Polling stopped; resume with --task-id, do not resubmit');

const savedBytes=await download(taskId);
console.log('Downloaded: h3-output.mp4 bytes:',savedBytes);

视频 API 对接

视频模型与参数

目录、计费单位与真实验证范围分开显示,不猜价、不混用协议。

按系列选择型号,点击完整模型 ID 即可打开对应请求模板。下方保留原始参数、价格日期与验证范围。

手机端可横向滑动表格,查看完整型号、验证范围和上架条件。

H31 款

当前模型 ID单位本站价(元/条或元/秒)可配置时长分辨率画幅验证范围
h3按秒480p 0.02 / 720p 0.08 / 1080p 0.40 元/秒官方计费上限15秒;上游支持需另验480p / 720p / 1080p 为价格阶梯,非出片保证按 H3 的 aspect_ratio 参数配置已同步 · 本轮未付费复测

Seedance 2.04 款

当前模型 ID单位本站价(元/条或元/秒)可配置时长分辨率画幅验证范围
seedance-2.0-900-特惠按条0.84–15 秒480p / 720p16:9 / 9:16 / 1:1实测通过 · 5秒720p
sd-2.0-933-720-fast-原生真人按条2.94–15 秒720p16:9 / 9:16 / 1:1目录可见 · 尚未通过验收
seedance-2.0-933-特惠按条1.54–15 秒480p / 720p / 1080p16:9 / 9:16 / 1:1目录可见 · 尚未通过验收
seedance2.0-mini-A按条1.25–15 秒480p / 720p16:9 / 9:16 / 1:1目录可见 · 尚未通过验收

Seedance 2.54 款

当前模型 ID单位本站价(元/条或元/秒)可配置时长分辨率画幅验证范围
seedance-2.5-900-特惠按条1.04–30 秒480p / 720p16:9 / 9:16 / 1:1目录可见 · 尚未通过验收
seedance-2.5-1010-特惠按条1.84–30 秒480p / 720p16:9 / 9:16 / 1:1目录可见 · 尚未通过验收
seedance-2.5-3010-特惠按条4.04–30 秒480p / 720p16:9 / 9:16 / 1:1目录可见 · 尚未通过验收
seedance2.5-pro-G1按条15.016–30 秒720p16:9 / 9:16 / 1:1目录可见 · 尚未通过验收

Grok1 款

当前模型 ID单位本站价(元/条或元/秒)可配置时长分辨率画幅验证范围
grok-1.5按条1.53–15 秒720p16:9 / 9:16 / 1:1目录可见 · 尚未通过验收

MiniMax H31 款

当前模型 ID单位本站价(元/条或元/秒)可配置时长分辨率画幅验证范围
minimax-h3-933-2k-支持真人按条1.24–15 秒2k9:16 / 1:1 / 3:4 / 4:3 / 16:9目录可见 · 尚未通过验收

视频 API 对接

视频请求 · JSON / curl / PowerShell / Python

只给当前可选型号生成示例;不会在浏览器自动花额度。

POST/v1/videos

    请求 JSON · 不会自动提交
    {
      "model": "seedance-2.0-900-特惠",
      "prompt": "一只蓝色纸船在平静水面缓慢漂浮,镜头固定,无人物,无文字",
      "duration": 5,
      "ratio": "16:9",
      "resolution": "720p"
    }
    Bash / curl · 创建、查询、下载
    # KEY 由服务端环境安全注入;本页不会发送请求。
    # 将上面的 JSON 保存为 video-request.json。
    curl --fail-with-body --retry 0 "https://xcmapi.org/v1/videos" \
      -H "Authorization: Bearer $XCM_API_KEY" \
      -H "Content-Type: application/json" --data-binary @video-request.json
    
    # 保存 response.id / task_id / request_id,再查询;不是重复创建。
    curl --fail-with-body "https://xcmapi.org/v1/videos/TASK_ID" \
      -H "Authorization: Bearer $XCM_API_KEY"
    
    # 仅在明确完成后取文件;不要使用 --location-trusted。
    curl --fail-with-body "https://xcmapi.org/v1/videos/TASK_ID/content" \
      -H "Authorization: Bearer $XCM_API_KEY" --output "result.mp4"
    PowerShell · UTF-8 中文模型
    $headers = @{ Authorization = "Bearer $env:XCM_API_KEY" }
    $body = @{ model="seedance-2.0-900-特惠"; prompt="蓝色纸船漂浮,无人物无文字"; duration=5; ratio="16:9"; resolution="720p" } | ConvertTo-Json
    # 只创建一次;超时或422时保留业务单,不自动重发。
    $r = Invoke-RestMethod -Method Post -Uri "https://xcmapi.org/v1/videos" -Headers $headers -ContentType "application/json; charset=utf-8" -Body ([Text.Encoding]::UTF8.GetBytes($body))
    $task = if ($null -ne $r.data -and ($r.data -is [System.Collections.IDictionary] -or $r.data -is [pscustomobject])) { $r.data } else { $r }
    $taskId = if ($task.id) { $task.id } elseif ($task.task_id) { $task.task_id } else { $task.request_id }
    if ($taskId -isnot [string] -or $taskId -cnotmatch '\A[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}\z') {
        throw '响应没有有效任务ID;保留业务单核对,不要重发POST。'
    }
    # 保存 taskId,再以同一 KEY 查询;生产调用应使用有上限的GET退避。
    Invoke-RestMethod -Uri "https://xcmapi.org/v1/videos/$taskId" -Headers $headers
    # 确认完成后:下句仅支持直接二进制,不跟随带KEY的重定向。
    # 若 /content 返回30x或JSON下载地址,请使用下方Python安全下载流程。
    Invoke-WebRequest -Uri "https://xcmapi.org/v1/videos/$taskId/content" -Headers $headers -MaximumRedirection 0 -OutFile "D:\video-tasks\result.mp4"
    响应结构示例 · 非真实任务
    {
      "accepted": {
        "id": "TASK_ID",
        "status": "pending",
        "progress": 0
      },
      "completed": {
        "id": "TASK_ID",
        "status": "done",
        "progress": 100,
        "video": {
          "url": "https://xcmapi.org/v1/videos/TASK_ID/content"
        }
      }
    }
    Python 客户端 · 一次创建 / 回执恢复 / 下载
    完整 Python 客户端
    """Mixed video client. Seedance 2.0 at 5s/720p has one real end-to-end verified case.
    
    Use XCM_API_KEY only in a server environment. POST is never automatically retried.
    Submit requires a new explicit receipt path; resume uses its saved task ID.
    """
    import argparse,json,os,re,time
    from pathlib import Path
    from urllib import request,error,parse
    MODELS={
     'seedance-2.0-900-特惠':(5, '720p'),
     'seedance-2.5-900-特惠':(4, '480p'),
     'grok-1.5':(3, '720p'),
     'minimax-h3-933-2k-支持真人':(15, '2k'),
     'sd-2.0-933-720-fast-原生真人':(5, '720p'),
     'seedance-2.0-933-特惠':(5, '720p'),
     'seedance-2.5-1010-特惠':(5, '720p'),
     'seedance-2.5-3010-特惠':(5, '720p'),
     'seedance2.0-mini-A':(5, '720p'),
     'seedance2.5-pro-G1':(30, '720p'),
    }
    class NoRedirect(request.HTTPRedirectHandler):
     def redirect_request(self,*args):return None
    
    def api(method,path,body=None):
     origin=os.environ.get('XCM_API_ORIGIN','https://xcmapi.org').rstrip('/');u=parse.urlsplit(origin)
     if u.scheme!='https' or not u.netloc or u.path or u.username or u.password or u.query or u.fragment:raise ValueError('XCM_API_ORIGIN must be an HTTPS origin, without /v1')
     key=os.environ.get('XCM_API_KEY','')
     if not key or '\n' in key or '\r' in key:raise ValueError('Set XCM_API_KEY in the server environment')
     data=json.dumps(body,ensure_ascii=False).encode() if body is not None else None
     req=request.Request(origin+path,data=data,method=method,headers={'Authorization':'Bearer '+key,'Content-Type':'application/json'})
     try:r=request.build_opener(NoRedirect).open(req,timeout=100 if method=='POST' else 35)
     except error.HTTPError as e:r=e
     with r:
      code=r.status;raw=r.read(2*1024*1024+1);retry=r.headers.get('Retry-After','5');ct=r.headers.get('Content-Type','')
     if len(raw)>2*1024*1024 or 'json' not in ct.lower():raise RuntimeError('Not a bounded JSON API response; do not resubmit POST')
     try:value=json.loads(raw)
     except (ValueError,UnicodeError):raise RuntimeError('Invalid JSON response; do not resubmit POST') from None
     return code,value,min(60,max(1,int(retry))) if retry.isdigit() else 5
    
    def unwrap(value):
     if not isinstance(value,dict):raise RuntimeError('Expected a task object')
     return value['data'] if isinstance(value.get('data'),dict) else value
    
    def extract_id(value):
     v=unwrap(value)
     if v.get('error'):raise RuntimeError('No accepted task; reconcile before trying a new business order')
     ident=v.get('task_id') or v.get('id') or v.get('request_id')
     if not isinstance(ident,str) or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}',ident):raise RuntimeError('No valid task ID; never retry this POST automatically')
     return ident
    
    def replace_receipt(path,data):
     temp=path.with_name(path.name+'.next')
     # Contains only task metadata, never KEY, prompt, materials or a signed media URL.
     with temp.open('x',encoding='utf8') as f:json.dump(data,f,ensure_ascii=False,indent=2)
     for attempt in range(6):
      try:temp.replace(path);return
      except PermissionError:
       if attempt==5:raise
       time.sleep(.1)
    
    def submit_once(path,model,prompt,duration=None,resolution=None):
     if model not in MODELS:raise ValueError('Unknown complete model ID')
     if not path.is_absolute():raise ValueError('Choose an explicit absolute receipt path')
     default_seconds,default_resolution=MODELS[model]
     receipt={'model':model,'requested_duration':duration if duration is not None else default_seconds,'requested_resolution':resolution or default_resolution,'state':'creation_uncertain','task_id':None}
     # Reserve a new business receipt before any network side effect. Existing receipts
     # cannot be overwritten to accidentally create another billable task.
     with path.open('x',encoding='utf8') as f:json.dump(receipt,f,ensure_ascii=False)
     try:
      status,response,_=api('POST','/v1/videos',{'model':model,'prompt':prompt,'duration':duration if duration is not None else default_seconds,'resolution':resolution or default_resolution,'ratio':'16:9'})
     except (TimeoutError,error.URLError,RuntimeError):
      raise RuntimeError('Creation outcome is unknown. Keep this receipt and reconcile; do not resubmit.') from None
     if status not in (200,201,202):raise RuntimeError(f'Create HTTP {status}; keep the receipt, inspect the cause, and do not automatically retry POST')
     ident=extract_id(response);receipt.update(task_id=ident,state='pending')
     try:replace_receipt(path,receipt)
     except OSError:raise RuntimeError(f'Accepted task ID: {ident}. Receipt update failed; save this ID and never repeat POST.') from None
     return ident
    
    def poll(ident,max_attempts=180,max_elapsed=1800):
     if not isinstance(ident,str) or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}',ident):raise ValueError('Invalid task ID')
     deadline=time.monotonic()+max_elapsed
     for _ in range(max_attempts):
      if time.monotonic()>deadline:break
      try:code,response,retry=api('GET','/v1/videos/'+parse.quote(ident,safe=''))
      except (TimeoutError,error.URLError):time.sleep(5);continue
      if code in (429,502,503,504):time.sleep(retry);continue
      if code!=200:raise RuntimeError(f'Query HTTP {code}; retain the task ID')
      data=unwrap(response);state=str(data.get('status','')).lower()
      if state in ('failed','error','expired','cancelled','canceled'):raise RuntimeError('Task ended without success; reconcile its usage before a new submission')
      if state in ('done','completed','succeeded','success'):
       return data  # /content may provide the media even when polling has no URL.
      if state not in ('pending','queued','processing','running','in_progress'):raise RuntimeError('Unknown task state; do not treat it as success')
      time.sleep(5)
     raise TimeoutError('Polling stopped, not cancelled. Resume with the same receipt; do not create another task')
    
    def download(ident, path):
        """Fetch /content, following at most five HTTPS redirects/JSON links."""
        import re
        from urllib import request as http, error as errors, parse as urls
        if not isinstance(ident, str) or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_.:-]{0,159}', ident):
            raise ValueError('Invalid task ID')
        if not path.is_absolute():
            raise ValueError('Choose an explicit absolute output path')
        if path.exists():
            raise FileExistsError('Output exists; choose a new path')
    
        def checked_url(value, base=None):
            if not isinstance(value, str) or not value or any(ord(c) <= 32 or ord(c) == 127 for c in value) or '\\' in value:
                raise ValueError('Invalid download URL')
            target = urls.urljoin(base, value) if base else value
            p = urls.urlsplit(target)
            if p.scheme != 'https' or not p.hostname or p.username is not None or p.password is not None or p.fragment or p.port == 0:
                raise ValueError('Download URL must be HTTPS without credentials or fragment')
            return target, (p.hostname.lower(), p.port or 443)
    
        origin = os.environ.get('XCM_API_ORIGIN', 'https://xcmapi.org').rstrip('/')
        origin, auth_origin = checked_url(origin)
        parsed = urls.urlsplit(origin)
        if parsed.path or parsed.query:
            raise ValueError('Use an HTTPS origin without /v1')
        key = os.environ.get('XCM_API_KEY', '')
        if not key.strip() or '\n' in key or '\r' in key:
            raise ValueError('Set XCM_API_KEY in the server environment')
        url = origin + '/v1/videos/' + urls.quote(ident, safe='') + '/content'
        opener = http.build_opener(NoRedirect)
        send_auth = True
        hops = 0
        limit = 250 * 1024 * 1024
        part = path.with_name(path.name + '.partial')
        created = False
        try:
            while True:
                url, current_origin = checked_url(url)
                send_auth = send_auth and current_origin == auth_origin
                headers = {'Accept': 'video/mp4, application/octet-stream, application/json'}
                if send_auth:
                    headers['Authorization'] = 'Bearer ' + key
                req = http.Request(url, headers=headers)
                try:
                    response = opener.open(req, timeout=90)
                except errors.HTTPError as exc:
                    response = exc
                with response:
                    kind = response.headers.get('Content-Type', '').split(';')[0].strip().lower()
                    if response.status in (301, 302, 303, 307, 308):
                        target = response.headers.get('Location')
                    elif response.status != 200:
                        raise RuntimeError(f'Download HTTP {response.status}; retain task ID')
                    elif kind == 'application/json' or kind.endswith('+json'):
                        raw = response.read(1024 * 1024 + 1)
                        if len(raw) > 1024 * 1024:
                            raise RuntimeError('Download JSON exceeds 1MiB')
                        try:
                            data = json.loads(raw)
                        except (ValueError, UnicodeError):
                            raise RuntimeError('Invalid download JSON') from None
                        if isinstance(data, dict) and isinstance(data.get('data'), dict):
                            data = data['data']
                        if not isinstance(data, dict):
                            raise RuntimeError('Expected a download URL object')
                        video = data.get('video')
                        results = data.get('result_urls')
                        target = (data.get('url') or data.get('download_url') or data.get('video_url') or data.get('content_url')
                                  or (video.get('url') if isinstance(video, dict) else None)
                                  or (results[0] if isinstance(results, list) and results else None))
                    else:
                        if kind not in ('video/mp4', 'application/octet-stream'):
                            raise RuntimeError('Not an MP4 download response')
                        length = response.headers.get('Content-Length')
                        if length is not None and (not length.isdigit() or not 0 < int(length) <= limit):
                            raise RuntimeError('Invalid or excessive media length')
                        first = response.read(4096)
                        if len(first) < 12 or first[4:8] != b'ftyp':
                            raise RuntimeError('Response is not an MP4')
                        size = len(first)
                        with part.open('xb') as stream:
                            created = True
                            stream.write(first)
                            while True:
                                chunk = response.read(1024 * 1024)
                                if not chunk:
                                    break
                                size += len(chunk)
                                if size > limit:
                                    raise RuntimeError('Media exceeds the 250MiB client limit')
                                stream.write(chunk)
                        if length is not None and size != int(length):
                            raise RuntimeError('Incomplete media response; retain task ID')
                        break
                    if hops >= 5:
                        raise RuntimeError('Too many download redirects/JSON links (maximum 5)')
                    url, _ = checked_url(target, url)
                    hops += 1
            # Publish exclusively: a concurrent download must not overwrite this file.
            for attempt in range(6):
                try:
                    os.link(part, path)
                    break
                except PermissionError:
                    if attempt == 5:
                        raise
                    time.sleep(.1)
            return size
        finally:
            if created:
                part.unlink(missing_ok=True)
    
    def main():
     parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--receipt',type=Path,required=True);mode=parser.add_mutually_exclusive_group(required=True);mode.add_argument('--submit',action='store_true');mode.add_argument('--resume',action='store_true');parser.add_argument('--model',choices=MODELS);parser.add_argument('--prompt',default='A paper boat floats on a calm pond, no people, static camera.');parser.add_argument('--duration',type=int);parser.add_argument('--resolution');parser.add_argument('--output',type=Path);args=parser.parse_args()
     if not args.receipt.is_absolute():parser.error('--receipt must be an explicit absolute path')
     if args.submit:
      if not args.model:parser.error('--submit requires --model')
      ident=submit_once(args.receipt,args.model,args.prompt,args.duration,args.resolution)
     else:
      receipt=json.loads(args.receipt.read_text(encoding='utf8'));ident=receipt.get('task_id')
      if not ident:raise SystemExit('Receipt has no confirmed task ID. Reconcile the original submission; do not submit again')
     poll(ident);receipt=json.loads(args.receipt.read_text(encoding='utf8'));receipt['state']='done';replace_receipt(args.receipt,receipt)
     if args.output:
      size=download(ident,args.output);receipt['downloaded_file']=str(args.output);receipt['downloaded_bytes']=size;replace_receipt(args.receipt,receipt)
      print('Downloaded:',args.output,'bytes:',size)
     print('Completed task:',ident)
     print('Download with the same KEY through: /v1/videos/'+parse.quote(ident,safe='')+'/content')
     print('Do not send your KEY to any external media URL.')
    if __name__=='__main__':main()
    Python 启动与恢复
    # 首次创建 + 查询 + 下载:一次POST,输出路径不可覆盖已有文件。
    python video_client.py --receipt "D:\video-tasks\order-001.json" --submit --model "seedance-2.0-900-特惠" --duration 5 --resolution 720p --output "D:\video-tasks\result.mp4"
    # 创建已受理但查询中断:只恢复GET,不再POST。
    python video_client.py --receipt "D:\video-tasks\order-001.json" --resume --output "D:\video-tasks\result.mp4"
    用途混合视频H3 专用
    模型当前 10 款完整 IDh3
    请求JSON /v1/videos按 H3 专用章节
    素材images / videos / audios / materials,依型号image / last_frame / reference_images / reference_audios
    KEY视频综合分组-全球顶尖视频模型-渠道版同一分组;仍使用 H3 独立请求参数
    给 AI / SDK 的机器可读契约
    接入契约 JSON
    {
      "version": 1,
      "status": "partial",
      "surface": "production",
      "observed_at": "2026-10-01T09:47:31+08:00",
      "entry_enabled": true,
      "verified_end_to_end_models": [
        {
          "id": "seedance-2.0-900-特惠",
          "duration": 5,
          "resolution": "720p",
          "actual_billing": 0.8,
          "billing_records": 1,
          "repeat_gets_did_not_add_billing": true
        }
      ],
      "upstream_catalog_count": 12,
      "current_key_model_count": 11,
      "pending_model_count": 3,
      "origin": "https://xcmapi.org",
      "authentication": "Server environment XCM_API_KEY; Authorization: Bearer",
      "create": {
        "method": "POST",
        "path": "/v1/videos",
        "content_type": "application/json"
      },
      "poll": {
        "method": "GET",
        "path": "/v1/videos/{task_id}"
      },
      "download": {
        "method": "GET",
        "path": "/v1/videos/{task_id}/content"
      },
      "create_auto_retry": false,
      "create_idempotency_header_supported": false,
      "current_models_billing_unit": "per_request",
      "models": [
        {
          "id": "seedance-2.0-900-特惠",
          "duration_min": 4,
          "duration_max": 15,
          "duration_default": 5,
          "resolutions": [
            "480p",
            "720p"
          ],
          "ratio_values": [
            "16:9",
            "9:16",
            "1:1"
          ],
          "duration_max_by_resolution": {},
          "max_reference_images": 9,
          "max_reference_audios": 3,
          "max_reference_videos": 3,
          "example": {
            "model": "seedance-2.0-900-特惠",
            "prompt": "一只蓝色纸船在平静水面缓慢漂浮,镜头固定,无人物,无文字",
            "duration": 5,
            "ratio": "16:9",
            "resolution": "720p"
          },
          "per_request_price": 0.8,
          "end_to_end_verified": true
        },
        {
          "id": "seedance-2.5-900-特惠",
          "duration_min": 4,
          "duration_max": 30,
          "duration_default": 5,
          "resolutions": [
            "480p",
            "720p"
          ],
          "ratio_values": [
            "16:9",
            "9:16",
            "1:1"
          ],
          "duration_max_by_resolution": {},
          "max_reference_images": 9,
          "max_reference_audios": 3,
          "max_reference_videos": 3,
          "example": {
            "model": "seedance-2.5-900-特惠",
            "prompt": "一只蓝色纸船在平静水面缓慢漂浮,镜头固定,无人物,无文字",
            "duration": 5,
            "ratio": "16:9",
            "resolution": "480p"
          },
          "per_request_price": 1,
          "end_to_end_verified": false
        },
        {
          "id": "grok-1.5",
          "duration_min": 3,
          "duration_max": 15,
          "duration_default": 5,
          "resolutions": [
            "720p"
          ],
          "ratio_values": [
            "16:9",
            "9:16",
            "1:1"
          ],
          "duration_max_by_resolution": {},
          "max_reference_images": 7,
          "max_reference_audios": 0,
          "max_reference_videos": 0,
          "example": {
            "model": "grok-1.5",
            "prompt": "一只蓝色纸船在平静水面缓慢漂浮,镜头固定,无人物,无文字",
            "duration": 5,
            "ratio": "16:9",
            "resolution": "720p"
          },
          "per_request_price": 1.5,
          "end_to_end_verified": false
        },
        {
          "id": "minimax-h3-933-2k-支持真人",
          "duration_min": 4,
          "duration_max": 15,
          "duration_default": 15,
          "resolutions": [
            "2k"
          ],
          "ratio_values": [
            "9:16",
            "1:1",
            "3:4",
            "4:3",
            "16:9"
          ],
          "duration_max_by_resolution": {},
          "max_reference_images": 32,
          "max_reference_audios": 3,
          "max_reference_videos": 3,
          "example": {
            "model": "minimax-h3-933-2k-支持真人",
            "prompt": "一只蓝色纸船在平静水面缓慢漂浮,镜头固定,无人物,无文字",
            "duration": 15,
            "ratio": "16:9",
            "resolution": "2k"
          },
          "per_request_price": 1.2,
          "end_to_end_verified": false
        },
        {
          "id": "sd-2.0-933-720-fast-原生真人",
          "duration_min": 4,
          "duration_max": 15,
          "duration_default": 5,
          "resolutions": [
            "720p"
          ],
          "ratio_values": [
            "16:9",
            "9:16",
            "1:1"
          ],
          "duration_max_by_resolution": {},
          "max_reference_images": 9,
          "max_reference_audios": 3,
          "max_reference_videos": 3,
          "example": {
            "model": "sd-2.0-933-720-fast-原生真人",
            "prompt": "一只蓝色纸船在平静水面缓慢漂浮,镜头固定,无人物,无文字",
            "duration": 5,
            "ratio": "16:9",
            "resolution": "720p"
          },
          "per_request_price": 2.9,
          "end_to_end_verified": false
        },
        {
          "id": "seedance-2.0-933-特惠",
          "duration_min": 4,
          "duration_max": 15,
          "duration_default": 5,
          "resolutions": [
            "480p",
            "720p",
            "1080p"
          ],
          "ratio_values": [
            "16:9",
            "9:16",
            "1:1"
          ],
          "duration_max_by_resolution": {},
          "max_reference_images": 9,
          "max_reference_audios": 3,
          "max_reference_videos": 3,
          "example": {
            "model": "seedance-2.0-933-特惠",
            "prompt": "一只蓝色纸船在平静水面缓慢漂浮,镜头固定,无人物,无文字",
            "duration": 5,
            "ratio": "16:9",
            "resolution": "720p"
          },
          "per_request_price": 1.5,
          "end_to_end_verified": false
        },
        {
          "id": "seedance-2.5-1010-特惠",
          "duration_min": 4,
          "duration_max": 30,
          "duration_default": 5,
          "resolutions": [
            "480p",
            "720p"
          ],
          "ratio_values": [
            "16:9",
            "9:16",
            "1:1"
          ],
          "duration_max_by_resolution": {},
          "max_reference_images": 10,
          "max_reference_audios": 10,
          "max_reference_videos": 3,
          "example": {
            "model": "seedance-2.5-1010-特惠",
            "prompt": "一只蓝色纸船在平静水面缓慢漂浮,镜头固定,无人物,无文字",
            "duration": 5,
            "ratio": "16:9",
            "resolution": "720p"
          },
          "per_request_price": 1.8,
          "end_to_end_verified": false
        },
        {
          "id": "seedance-2.5-3010-特惠",
          "duration_min": 4,
          "duration_max": 30,
          "duration_default": 5,
          "resolutions": [
            "480p",
            "720p"
          ],
          "ratio_values": [
            "16:9",
            "9:16",
            "1:1"
          ],
          "duration_max_by_resolution": {},
          "max_reference_images": 30,
          "max_reference_audios": 10,
          "max_reference_videos": 3,
          "example": {
            "model": "seedance-2.5-3010-特惠",
            "prompt": "一只蓝色纸船在平静水面缓慢漂浮,镜头固定,无人物,无文字",
            "duration": 5,
            "ratio": "16:9",
            "resolution": "720p"
          },
          "per_request_price": 4,
          "end_to_end_verified": false
        },
        {
          "id": "seedance2.0-mini-A",
          "duration_min": 5,
          "duration_max": 15,
          "duration_default": 5,
          "resolutions": [
            "480p",
            "720p"
          ],
          "ratio_values": [
            "16:9",
            "9:16",
            "1:1"
          ],
          "duration_max_by_resolution": {
            "480p": 15,
            "720p": 12
          },
          "max_reference_images": 9,
          "max_reference_audios": 3,
          "max_reference_videos": 3,
          "example": {
            "model": "seedance2.0-mini-A",
            "prompt": "一只蓝色纸船在平静水面缓慢漂浮,镜头固定,无人物,无文字",
            "duration": 5,
            "ratio": "16:9",
            "resolution": "720p"
          },
          "per_request_price": 1.2,
          "end_to_end_verified": false
        },
        {
          "id": "seedance2.5-pro-G1",
          "duration_min": 16,
          "duration_max": 30,
          "duration_default": 30,
          "resolutions": [
            "720p"
          ],
          "ratio_values": [
            "16:9",
            "9:16",
            "1:1"
          ],
          "duration_max_by_resolution": {},
          "max_reference_images": 30,
          "max_reference_audios": 10,
          "max_reference_videos": 0,
          "example": {
            "model": "seedance2.5-pro-G1",
            "prompt": "一只蓝色纸船在平静水面缓慢漂浮,镜头固定,无人物,无文字",
            "duration": 30,
            "ratio": "16:9",
            "resolution": "720p"
          },
          "per_request_price": 15,
          "end_to_end_verified": false
        }
      ],
      "pending_models": [
        {
          "id": "seedance2.0-720-满血",
          "unit": "per_request",
          "currently_enabled": false,
          "configured_price_rmb": 4.8,
          "reason_code": "missing_from_upstream_pricing_and_authenticated_model_catalog",
          "source_catalog_missing": true,
          "price_is_not_an_available_offer": true
        },
        {
          "id": "seedance2.5-满血",
          "unit": "per_second",
          "currently_enabled": false,
          "configured_price_tiers": [
            {
              "resolution": "480p",
              "price_rmb_per_second": 0.55
            },
            {
              "resolution": "720p",
              "price_rmb_per_second": 0.85
            },
            {
              "resolution": "1080p",
              "price_rmb_per_second": 1.3
            }
          ],
          "upstream_duration_min": 4,
          "upstream_duration_max": 30,
          "official_billing_duration_max": 15,
          "candidate_duration_min": 4,
          "candidate_duration_max": 15,
          "safe_subset_enabled": false,
          "reason_code": "duration_validation_and_billing_acceptance_required",
          "price_is_not_an_available_offer": true
        },
        {
          "id": "seedance2.5-pro-G2",
          "unit": "per_second",
          "currently_enabled": false,
          "configured_price_tiers": [
            {
              "resolution": "720p",
              "price_rmb_per_second": 0.5
            }
          ],
          "upstream_duration_min": 16,
          "upstream_duration_max": 30,
          "official_billing_duration_max": 15,
          "safe_duration_intersection": [],
          "reason_code": "no_safe_duration_intersection",
          "price_is_not_an_available_offer": true
        }
      ],
      "task_success_states": [
        "done",
        "completed",
        "succeeded",
        "success"
      ],
      "task_failure_states": [
        "failed",
        "error",
        "expired",
        "cancelled",
        "canceled"
      ],
      "h3_is_separate": true,
      "model_directory_is_not_a_price_api": true,
      "configuration_observed_at": "2026-10-04T03:05:23.710803+00:00",
      "group": {
        "id": 94,
        "name": "视频综合分组-全球顶尖视频模型-渠道版",
        "configured_model_count": 11
      },
      "verified_end_to_end_observed_at": "2026-10-01T09:47:31+08:00",
      "upstream_catalog_observed_at": "2026-10-01T09:47:31+08:00",
      "upstream_catalog_count_scope": "historical_mixed_model_catalog",
      "current_key_model_count_scope": "configured_group_catalog_not_authenticated_key_probe",
      "authenticated_key_directory_verified_in_this_sync": false,
      "current_models_billing_unit_scope": "models_array_only_not_h3",
      "h3_separation_scope": "request_fields_and_protocol_not_key_group",
      "h3": {
        "id": "h3",
        "group_id": 94,
        "billing_mode": "video",
        "unit": "per_second",
        "currency": "RMB",
        "configured_price_tiers": [
          {
            "resolution": "480p",
            "price_rmb_per_second": 0.02
          },
          {
            "resolution": "720p",
            "price_rmb_per_second": 0.08
          },
          {
            "resolution": "1080p",
            "price_rmb_per_second": 0.4
          }
        ],
        "configuration_synchronized": true,
        "end_to_end_verified_in_this_sync": false,
        "priced_resolutions_are_not_output_guarantees": true,
        "billing_duration_ceiling_seconds": 15,
        "price_snapshot_at": "2026-10-04T03:05:23.710803+00:00",
        "request_fields_separate_from_mixed_models": true,
        "documentation_page": "h3-video"
      }
    }

    视频 API 对接

    视频计费 · 防重建、防重复扣费与错误处理

    按真实任务记录核查费用,不靠目录、健康接口或空请求冒充完整验收。

    阶段动作计费/重试
    创建返回ID立即保存回执只代表已受理,不代表出片/结算
    pending / processing / queued每5–10秒GET,遇429/5xx退避不要重复POST
    done / completed同一KEY下载content并核对用量按当前模型的计费单位结算
    failed / expired / canceled保留任务ID核查不能凭客户端失败推断上游没有收费
    422 / 创建结果不明停止自动提交先核查原请求,不另建任务替代
    现象先做什么禁止动作
    400核对完整模型名、JSON字段和参数范围不要删除中文后缀或套用别的模型规格
    401 / 403核对KEY状态、分组及权限;向客服提供脱敏请求ID不要公开完整KEY,不向第三方送KEY
    404核对本站地址、创建时KEY和任务ID不把随机404当成真实任务测试通过
    422 / 创建超时保存回执、请求时间与模型,请求核查禁止自动重复POST
    429 / 502 / 503 / 504 查询有限GET退避;保留原任务ID不要为绕开查询错误重新生成
    完成但下载失败继续核查同一任务的content,检查响应类型不把HTML保存成MP4,不跟随带KEY的外站链接
    重复查询出现额外费用保存任务ID和账单时间联系核查不要自行改余额或复制业务单

    上线与排查

    生产 API 对接方案

    将鉴权、请求生命周期、任务恢复和计费核对放到服务端。

    1. CLIENT用户请求
    2. YOUR SERVER权限 / 限流 / 业务单
    3. SITE API文本或视频任务
    4. WORKER保存结果与账单关联
    模块实现要求验收标准
    鉴权配置以环境变量 / 密钥服务保存 KEY;按业务选择明确的分组请求中仅向本站携带 KEY,错误日志脱敏
    模型选择按 KEY 获取 /v1/models,保存用户选择的完整模型 ID模型不存在时明确报错,不静默切换昂贵模型
    文本转发透传当前协议支持的字段;设置连接和读取超时JSON / SSE 各自解析,终态与业务成功分开判断
    视频创建先建立本地业务单;创建成功立即记录任务 ID重复点击只创建一次,进程重启不会自动重发 POST
    状态 Worker3–5 秒轮询,有限并发;429 依据 Retry-After 退避断线后可通过 task_id 恢复,失败与过期状态终止
    结果存储记录返回格式与过期时间;必要时安全保存生成文件不会把本站 KEY 传给外部下载 URL
    计费关联业务单与 request_id / task_id、模型、分辨率、用量记录关联调价后采用实时分组价,异常可追踪,不推测退款政策

    超时建议(客户端配置,不是服务承诺)

    模型目录 30 秒;文本请求按模型设置合理的首包及读取超时;视频创建可用 60 秒、单次查询 30 秒、后台轮询最多 120 次。每次网络请求自身也有超时,因此总耗时可能高于轮询间隔之和。长期任务应由后台 Worker 承担,不依赖浏览器页面一直打开。

    上线与排查

    错误码与排查

    先按请求阶段排查,再检查模型、分组和协议。

    状态码常见场景处理方式
    400字段、枚举、媒体组合或 JSON 不合法按错误消息修正请求。
    401Key 缺失、无效或已撤销检查鉴权头和 Key。
    402余额、订阅或额度不足检查账户余额和分组额度。
    403分组未开启对应能力更换有权限的 Key 或联系管理员。
    404模型、任务或接口不存在检查模型目录、任务 ID 和路径。
    429频率或并发受限降低并发并稍后重试。
    502/503渠道不可用或暂无可调度账号稍后重试;持续出现时联系售后。
    现象优先检查
    200 但没有内容响应终态是否 failed/incomplete,SSE 是否有 error 事件
    工具调用后续接失败call_id 是否匹配、工具结果类型是否正确、前次 output 是否完整
    视频参数 400duration/resolution 范围、素材数量、首尾帧和音频是否混用
    视频接口 404该分组是否支持视频路径;内测混合分组目前暂未开放
    任务已完成但下载失败结果 URL 是否过期、是否使用正确 KEY / 任务归属、content 是否可用

    上线与排查

    接入验收清单

    完成协议验收后再开通业务流量。不要用一次成功代表所有模型和素材组合都可用。

    • □ 未登录可阅读文档和价格;API 使用无效 KEY 时正确拒绝。
    • □ 文本、图片、视频各使用正确分组,模型 ID 完整。
    • □ 非流式 JSON 与流式 SSE 的成功、失败和中断均可处理。
    • □ 视频创建只提交一次;查询成功、失败、未知、超时均有终态处理。
    • □ 进程重启能凭 task_id 续查;并发请求不会重复建单。
    • □ 480p/720p/1080p 和按条/按秒分别核对;售价只乘一次倍率。
    • □ 返回链接的下载不携带本站 KEY 给其他域名。
    • □ 素材超限、无效 URL、过期任务、429 与 5xx 有明确处理。
    • □ 测试使用独立 KEY 和额度;生成视频会产生真实费用。
    • □ 混合视频仍为内测时,不发布可生成的承诺或执行示例。

    客户端配置

    环境与依赖

    使用对应客户端的配置说明;模型权限与协议仍以 KEY 分组为准。

    1

    打开终端,确认已安装 Node.js 20 或更高版本。

    2

    如果命令不存在,请安装 Node.js 后重新打开终端。

    检查版本
    node --version
    
    npm --version

    按 Esc 或点击关闭返回教程。