# 辰渡 API 文档 这份文件按网页顺序收录全部文档页,每页另有自己的 Markdown 文件。 --- ## 快速开始 三步生成第一个视频:创建 API 密钥,提交任务,取回结果。 你需要一个已开通的账户,余额够预留一次生成。下面以 Seedance 2.0 的文生视频为例,时长、分辨率和画幅都用默认值(5 秒、720p、自适应)。 **接入地址**:向开户人员索取接入地址。 ### 1. 创建 API 密钥 在控制台的「[API 密钥](./integration.html)」页点「创建 API 密钥」,把它和上面的接入地址存进服务器的环境变量。同一个 API 密钥可以调用全部模型。 ```bash export CHENVOYA_BASE_URL="接入地址" export CHENVOYA_API_KEY="你的 API 密钥" ``` > **注意**:API 密钥只在服务器端使用,不要写进网页、App 或代码仓库,也不要贴进文档或日志。怀疑泄露时,在控制台停用它并新建一个。 ### 2. 提交生成任务 向 `POST /v1/video/generations` 发送模型和提示词。每次创建都带一个新的 `Idempotency-Key`:请求超时或断线时,用同一个键原样重试,不会重复创建任务,也不会重复扣费。 ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} printf '%s\n' "保存 IDEMPOTENCY_KEY,重试时原样复用:${IDEMPOTENCY_KEY}" >&2 curl --request POST "${CHENVOYA_BASE_URL}/v1/video/generations" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" \ --header "Idempotency-Key: ${IDEMPOTENCY_KEY}" \ --header 'Content-Type: application/json' \ --data '{ "model": "dreamina-seedance-2-0-260128", "content": [ { "type": "text", "text": "A paper boat crossing a moonlit lake." } ] }' ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} idempotency = os.environ.get("IDEMPOTENCY_KEY") or str(uuid.uuid4()) print("保存 IDEMPOTENCY_KEY,重试时原样复用:" + idempotency, file=sys.stderr) headers["Idempotency-Key"] = idempotency path = "/v1/video/generations" body_text = "{\"model\":\"dreamina-seedance-2-0-260128\",\"content\":[{\"type\":\"text\",\"text\":\"A paper boat crossing a moonlit lake.\"}]}" headers["Content-Type"] = "application/json" body = json.dumps(json.loads(body_text)).encode("utf-8") request = urllib.request.Request(base + path, data=body, headers=headers, method="POST") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const idempotency = process.env.IDEMPOTENCY_KEY || crypto.randomUUID(); console.error("保存 IDEMPOTENCY_KEY,重试时原样复用:" + idempotency); headers["Idempotency-Key"] = idempotency; const path = "/v1/video/generations"; let bodyText = "{\"model\":\"dreamina-seedance-2-0-260128\",\"content\":[{\"type\":\"text\",\"text\":\"A paper boat crossing a moonlit lake.\"}]}"; headers["Content-Type"] = "application/json"; const body = JSON.stringify(JSON.parse(bodyText)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "POST", headers, body: body}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "crypto/rand" "fmt" "io" "net/http" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } path := "/v1/video/generations" bodyText := "{\"model\":\"dreamina-seedance-2-0-260128\",\"content\":[{\"type\":\"text\",\"text\":\"A paper boat crossing a moonlit lake.\"}]}" request, err := http.NewRequest("POST", base+path, strings.NewReader(bodyText)) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) request.Header.Set("Content-Type", "application/json") idempotency := os.Getenv("IDEMPOTENCY_KEY") if idempotency == "" { idempotency = newOperationKey() } fmt.Fprintln(os.Stderr, "保存 IDEMPOTENCY_KEY,重试时原样复用:"+idempotency) request.Header.Set("Idempotency-Key", idempotency) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } // newOperationKey returns a random UUID; save it to retry with the same value. func newOperationKey() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { fail(err.Error()) } b[6] = b[6]&0x0f | 0x40 b[8] = b[8]&0x3f | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } ``` 接口立即返回 `202` 和任务 ID,生成在后台进行。 响应 202(节选): ```json { "id": "task_32ae2a5177b310d3bc1ec83a98f63889", "object": "video.task", "model": "dreamina-seedance-2-0-260128", "status": "queued", "settlement_status": "reserved", "quote": { "currency": "USD", "reserved_amount": "0.782453", "estimated_usage_upper_bound": { "video_tokens": 111779 } }, "created_at": 1793167380 } ``` > **提示**:金额先按请求的上限预留(`quote.reserved_amount`),完成后按实际用量结算,多出的部分释放;失败或取消的任务不收费。 ### 3. 取回结果 任务按下面的顺序推进,已成功、已失败和已取消都是终态。 `queued` (排队中) → `processing` (处理中) → `succeeded` (已成功);另外两个终态 `failed` (已失败) 和 `cancelled` (已取消) 都不收费。 #### 轮询任务 每隔几秒查询一次 `GET /v1/video/tasks/{task_id}`,直到 `status` 变成终态。 ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/video/tasks/${TASK_ID}" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} TASK_ID = os.environ["TASK_ID"] path = "/v1/video/tasks/{task_id}".replace("{task_id}", urllib.parse.quote(TASK_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const TASK_ID = process.env.TASK_ID; if (!TASK_ID) throw new Error("Set TASK_ID"); const path = "/v1/video/tasks/{task_id}".replace("{task_id}", encodeURIComponent(TASK_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } taskID := os.Getenv("TASK_ID") if taskID == "" { fail("Set TASK_ID") } path := strings.Replace("/v1/video/tasks/{task_id}", "{task_id}", url.PathEscape(taskID), 1) request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` 响应 200(节选): ```json { "id": "task_32ae2a5177b310d3bc1ec83a98f63889", "object": "video.task", "status": "succeeded", "output": { "assets": [ { "media_type": "video", "role": "result", "url": "https://media.example.invalid/download/result-0001?signature=example", "expires_at": 1793253785 } ] } } ``` 成功后,`output.assets[0].url` 就是视频的下载地址,最长 24 小时有效,且不晚于 `expires_at`(默认结果准备好后 7 天);再次查询任务可拿到新地址,过期后为空。把视频下载到自己的存储: ```bash curl --output video.mp4 "$VIDEO_URL" ``` #### 接收回调 不想轮询,可以让我们通知你:在控制台的「[回调](./callbacks.html)」页添加并验证一个 HTTPS 地址,创建任务时放进请求体的 `callback_url`。任务结束后你会收到 `task.completed`,费用结算后再收到 `task.billing_settled`。处理前先[验证签名](./api-docs-zh-callbacks-verify.md)。 ```json "callback_url": "https://example.com/webhooks/chenvoya" ``` ### 下一步 - [生成视频](./api-docs-zh-video-generate.md):文生视频、首帧、首尾帧、参考图和参考音视频的请求写法。 - [计费方式](./api-docs-zh-billing.md):创建时按上限预留,完成后按实际用量结算,失败和取消的任务不收费。 - [创建视频任务](./api-docs-zh-api-video-tasks-create.md):提交提示词和可选的图片、视频或音频,创建一个异步生成任务,立即返回任务 ID 和预留金额。 --- ## 认证 用 Bearer 方式在请求头发送 API 密钥。 每个请求都要在 `Authorization` 请求头带上 API 密钥。示例只引用环境变量 `CHENVOYA_API_KEY`,不写出密钥本身。 ### 发送 API 密钥 在 `Authorization` 请求头写 `Bearer`、一个空格和 API 密钥: ```bash Authorization: Bearer ${CHENVOYA_API_KEY} ``` 密钥缺失或无效时返回 `401 authentication_failed`,响应头 `WWW-Authenticate` 给出 Bearer 认证要求。 ### 创建和保管 - 在控制台的「[API 密钥](./integration.html)」页创建 API 密钥;同一个 API 密钥可以调用全部模型。 - 只在服务器端保存和使用 API 密钥,放在环境变量或密钥管理服务里;不要写进网页、App、代码仓库、文档或日志。 - 怀疑泄露时,在控制台停用它并新建一个,再替换服务器上的值。 ### 权限和团队 API 密钥属于团队。任务和素材按团队隔离:团队里任何一把 API 密钥创建的任务,都能用本团队的其他 API 密钥查询;其他团队的任务和素材一律返回 `task_not_found` 或 `asset_not_found`。 登记素材和查询素材需要 API 密钥有 `service.use` 权限,否则返回 `403 permission_denied`。 ### 认证相关的错误 | 状态码 | error.code | 怎么处理 | | --- | --- | --- | | 401 | `authentication_failed` | 检查 `Authorization: Bearer` 后面的 API 密钥。 | | 403 | `permission_denied` | 换一把有 `service.use` 权限的 API 密钥。 | | 409 | `key_provisioning_incomplete` | 这把 API 密钥的额度配置未完成:删除它,再新建一把。 | ### 下一步 - [请求约定](./api-docs-zh-conventions.md):接入地址、JSON、请求标识、时间、金额和翻页的通用规则。 - [快速开始](./api-docs-zh-quickstart.md):三步生成第一个视频:创建 API 密钥,提交任务,取回结果。 - [错误码](./api-docs-zh-api-errors.md):每个 error.code 的含义、状态码和出现的接口。 --- ## 请求约定 接入地址、JSON、请求标识、时间、金额和翻页的通用规则。 所有接口都遵守下面这些约定。具体字段见各接口页面。 ### 接入地址 把接入地址保存为 `CHENVOYA_BASE_URL`,不带末尾斜杠;文档里的路径都接在它后面,例如 `POST /v1/video/generations`。地址显示在「[API 概览](./api-docs-zh-api.md#connection)」和控制台的「接入」页。 ### JSON 请求体和响应体都是 JSON,发送请求体时带 `Content-Type: application/json`。字段名和取值都是英文标识,原样使用,不要翻译。 ### 请求标识 可以在请求头 `X-Request-ID` 里放自己的请求标识;无效或重复的值由服务替换。响应头 `X-Request-ID` 返回本次请求最终使用的标识,最长 64 个字符。排查问题时把它和错误响应里的 `trace_id` 一起提供给我们。 ### 标识 | 标识 | 格式 | 示例 | | --- | --- | --- | | 任务 ID | 字母开头,4–128 个字母、数字、`_` 或 `-` | `task_32ae2a5177b310d3bc1ec83a98f63889` | | 素材 ID | `ast-` 加 32 位小写十六进制 | `ast-` 开头 | | 请求关联标识 `request_id` | 与任务 ID 相同的格式 | `req_6acd96894df9798feb0ddb21e5a52d70` | | 费用关联标识 `billing_correlation_id` | 与任务 ID 相同的格式 | `billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0` | ### 时间 时间字段是自 Unix 纪元起的整数秒,例如 `created_at`。还没有发生的时间(如未开始任务的 `started_at`)为 `null`。 ### 金额 金额是美元,写成带六位小数的字符串,例如 `"0.361564"`。尚未确定的金额为 `null`。不要把金额转成浮点数再计算。 ### 翻页 列表接口用游标翻页:`page_size` 控制每页条数(1–100,默认 50),响应里的 `next_cursor` 放进下一次请求的 `cursor`。最后一页的 `next_cursor` 为 `null`。游标不透明,不要自己构造;不是服务为本团队这个列表签发的游标返回 `400 invalid_request`。 ### 错误 错误响应包含 `error` 对象和 `trace_id`。按 `error.code` 处理,详见「[处理错误](./api-docs-zh-errors.md)」。 ### 下一步 - [认证](./api-docs-zh-authentication.md):用 Bearer 方式在请求头发送 API 密钥。 - [幂等与重试](./api-docs-zh-idempotency.md):用同一个 Idempotency-Key 重试,不会重复创建任务,也不会重复扣费。 - [API 概览](./api-docs-zh-api.md):接入地址、认证、错误格式和全部接口。 --- ## 视频模型 Seedance 2.0、2.0 Fast 和 2.0 Mini 的能力和取值范围。 三个模型共用一个 API 密钥和同一套请求参数,在请求体的 `model` 里写模型 ID。 ### 可用模型 | 模型 | 模型 ID | 分辨率 | 时长(秒) | | --- | --- | --- | --- | | Seedance 2.0 | `dreamina-seedance-2-0-260128` | 480p · 720p · 1080p | 4–15,默认 5 | | Seedance 2.0 Fast | `dreamina-seedance-2-0-fast-260128` | 480p · 720p | 4–15,默认 5 | | Seedance 2.0 Mini | `dreamina-seedance-2-0-mini-260615` | 480p · 720p | 4–15,默认 5 | Seedance 2.0 Fast 和 Seedance 2.0 Mini 的 `resolution` 只接受 `480p` 或 `720p`。 ### 输入 - 一次请求最多 16 项 `content`:一段文本提示词,加最多 9 张图片、3 段参考视频和 3 段参考音频,素材合计不超过 15 份。 - 首帧请求用一张图片,首尾帧请求用两张,`first_frame` 和 `last_frame` 各最多一张。 - 参考视频和参考音频单段 2–15 秒,视频合计和音频合计都不超过 15 秒;参考音频必须和参考图片或参考视频一起使用。 - 图片和音频可以用 HTTPS 链接、`asset://` 素材引用或 base64 data URI;视频只能用 HTTPS 链接或 `asset://` 素材引用。详见「[生成视频](./api-docs-zh-video-generate.md#sources)」。 ### 输出 | 参数 | 取值 | 默认 | | --- | --- | --- | | `duration` | 4–15 秒的整数 | `5` | | `resolution` | `480p` · `720p` · `1080p` | `720p` | | `ratio` | `adaptive` · `21:9` · `16:9` · `4:3` · `1:1` · `3:4` · `9:16` | `adaptive` | | `generate_audio` | `true` · `false` | `false` | | `watermark` | `true` · `false` | `false` | | `seed` | -1 到 4294967295 的整数,-1 表示随机 | `-1` | | `return_last_frame` | `true` · `false` | `false` | ### 计费 视频生成按 `video_tokens` 等计量字段记录用量:创建时按上限预留,完成后按实际用量结算,详见「[计费方式](./api-docs-zh-billing.md)」。各模型的标价见「[价格](./public-prices.html)」页,本团队的价格在控制台查看。 ### 下一步 - [生成视频](./api-docs-zh-video-generate.md):文生视频、首帧、首尾帧、参考图和参考音视频的请求写法。 - [计费方式](./api-docs-zh-billing.md):创建时按上限预留,完成后按实际用量结算,失败和取消的任务不收费。 - [创建视频任务](./api-docs-zh-api-video-tasks-create.md):提交提示词和可选的图片、视频或音频,创建一个异步生成任务,立即返回任务 ID 和预留金额。 --- ## 生成视频 文生视频、首帧、首尾帧、参考图和参考音视频的请求写法。 所有场景都调用同一个接口 `POST /v1/video/generations`,区别只在 `content` 里放什么。任务、费用预留和幂等记录一起被接受,接口返回 `202` 和任务 ID,生成在后台进行。完整参数见「[创建视频任务](./api-docs-zh-api-video-tasks-create.md)」。 ### 文生视频 `content` 里只放一项 `text`,写清画面、动作和镜头。 ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} printf '%s\n' "保存 IDEMPOTENCY_KEY,重试时原样复用:${IDEMPOTENCY_KEY}" >&2 curl --request POST "${CHENVOYA_BASE_URL}/v1/video/generations" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" \ --header "Idempotency-Key: ${IDEMPOTENCY_KEY}" \ --header 'Content-Type: application/json' \ --data '{ "model": "dreamina-seedance-2-0-260128", "content": [ { "type": "text", "text": "A paper boat crossing a moonlit lake." } ] }' ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} idempotency = os.environ.get("IDEMPOTENCY_KEY") or str(uuid.uuid4()) print("保存 IDEMPOTENCY_KEY,重试时原样复用:" + idempotency, file=sys.stderr) headers["Idempotency-Key"] = idempotency path = "/v1/video/generations" body_text = "{\"model\":\"dreamina-seedance-2-0-260128\",\"content\":[{\"type\":\"text\",\"text\":\"A paper boat crossing a moonlit lake.\"}]}" headers["Content-Type"] = "application/json" body = json.dumps(json.loads(body_text)).encode("utf-8") request = urllib.request.Request(base + path, data=body, headers=headers, method="POST") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const idempotency = process.env.IDEMPOTENCY_KEY || crypto.randomUUID(); console.error("保存 IDEMPOTENCY_KEY,重试时原样复用:" + idempotency); headers["Idempotency-Key"] = idempotency; const path = "/v1/video/generations"; let bodyText = "{\"model\":\"dreamina-seedance-2-0-260128\",\"content\":[{\"type\":\"text\",\"text\":\"A paper boat crossing a moonlit lake.\"}]}"; headers["Content-Type"] = "application/json"; const body = JSON.stringify(JSON.parse(bodyText)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "POST", headers, body: body}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "crypto/rand" "fmt" "io" "net/http" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } path := "/v1/video/generations" bodyText := "{\"model\":\"dreamina-seedance-2-0-260128\",\"content\":[{\"type\":\"text\",\"text\":\"A paper boat crossing a moonlit lake.\"}]}" request, err := http.NewRequest("POST", base+path, strings.NewReader(bodyText)) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) request.Header.Set("Content-Type", "application/json") idempotency := os.Getenv("IDEMPOTENCY_KEY") if idempotency == "" { idempotency = newOperationKey() } fmt.Fprintln(os.Stderr, "保存 IDEMPOTENCY_KEY,重试时原样复用:"+idempotency) request.Header.Set("Idempotency-Key", idempotency) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } // newOperationKey returns a random UUID; save it to retry with the same value. func newOperationKey() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { fail(err.Error()) } b[6] = b[6]&0x0f | 0x40 b[8] = b[8]&0x3f | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } ``` ### 首帧和首尾帧 用 `image_url` 项放图片,`role` 写 `first_frame` 或 `last_frame`。只有一张图片时可以省略 `role`,按首帧处理;有多张图片时每张都要写 `role`。 ```json "content": [ {"type": "text", "text": "A paper boat crossing a moonlit lake."}, {"type": "image_url", "role": "first_frame", "image_url": {"url": "https://example.com/first.png"}}, {"type": "image_url", "role": "last_frame", "image_url": {"url": "https://example.com/last.png"}} ] ``` ### 参考图 `role` 写 `reference_image`,最多 9 张。下面的例子引用一个已登记的素材: ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} printf '%s\n' "保存 IDEMPOTENCY_KEY,重试时原样复用:${IDEMPOTENCY_KEY}" >&2 ASSET_ID_PATTERN='^ast-[0-9a-f]{32}$' if [[ ! ${ASSET_ID} =~ ${ASSET_ID_PATTERN} || ${ASSET_ID} == *[!A-Za-z0-9._-]* ]]; then printf '%s\n' '请先设置创建响应中的 ASSET_ID。' >&2 exit 1 fi curl --request POST "${CHENVOYA_BASE_URL}/v1/video/generations" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" \ --header "Idempotency-Key: ${IDEMPOTENCY_KEY}" \ --header 'Content-Type: application/json' \ --data '{ "model": "dreamina-seedance-2-0-260128", "duration": 5, "ratio": "16:9", "resolution": "720p", "content": [ { "type": "text", "text": "Animate the reviewed reference image." }, { "type": "image_url", "role": "reference_image", "image_url": { "url": "asset://'"${ASSET_ID}"'" } } ] }' ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} idempotency = os.environ.get("IDEMPOTENCY_KEY") or str(uuid.uuid4()) print("保存 IDEMPOTENCY_KEY,重试时原样复用:" + idempotency, file=sys.stderr) headers["Idempotency-Key"] = idempotency path = "/v1/video/generations" body_text = "{\"model\":\"dreamina-seedance-2-0-260128\",\"duration\":5,\"ratio\":\"16:9\",\"resolution\":\"720p\",\"content\":[{\"type\":\"text\",\"text\":\"Animate the reviewed reference image.\"},{\"type\":\"image_url\",\"role\":\"reference_image\",\"image_url\":{\"url\":\"asset://ast-bca04b1132034b9b42cb51fe29e9a0a7\"}}]}" ASSET_ID = os.environ["ASSET_ID"] if not re.fullmatch("^ast-[0-9a-f]{32}$", ASSET_ID): sys.exit("Set ASSET_ID to the ID from the creation response") body_text = body_text.replace("\"asset://ast-bca04b1132034b9b42cb51fe29e9a0a7\"", json.dumps("asset://" + ASSET_ID)) headers["Content-Type"] = "application/json" body = json.dumps(json.loads(body_text)).encode("utf-8") request = urllib.request.Request(base + path, data=body, headers=headers, method="POST") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const idempotency = process.env.IDEMPOTENCY_KEY || crypto.randomUUID(); console.error("保存 IDEMPOTENCY_KEY,重试时原样复用:" + idempotency); headers["Idempotency-Key"] = idempotency; const path = "/v1/video/generations"; let bodyText = "{\"model\":\"dreamina-seedance-2-0-260128\",\"duration\":5,\"ratio\":\"16:9\",\"resolution\":\"720p\",\"content\":[{\"type\":\"text\",\"text\":\"Animate the reviewed reference image.\"},{\"type\":\"image_url\",\"role\":\"reference_image\",\"image_url\":{\"url\":\"asset://ast-bca04b1132034b9b42cb51fe29e9a0a7\"}}]}"; const ASSET_ID = process.env.ASSET_ID; if (typeof ASSET_ID !== "string" || (new RegExp("^ast-[0-9a-f]{32}$")).exec(ASSET_ID)?.[0] !== ASSET_ID) throw new Error("Set ASSET_ID to the ID from the creation response"); bodyText = bodyText.replaceAll("\"asset://ast-bca04b1132034b9b42cb51fe29e9a0a7\"", JSON.stringify("asset://" + ASSET_ID)); headers["Content-Type"] = "application/json"; const body = JSON.stringify(JSON.parse(bodyText)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "POST", headers, body: body}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "crypto/rand" "fmt" "io" "net/http" "os" "regexp" "strconv" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } path := "/v1/video/generations" bodyText := "{\"model\":\"dreamina-seedance-2-0-260128\",\"duration\":5,\"ratio\":\"16:9\",\"resolution\":\"720p\",\"content\":[{\"type\":\"text\",\"text\":\"Animate the reviewed reference image.\"},{\"type\":\"image_url\",\"role\":\"reference_image\",\"image_url\":{\"url\":\"asset://ast-bca04b1132034b9b42cb51fe29e9a0a7\"}}]}" assetID := os.Getenv("ASSET_ID") if !regexp.MustCompile(`\A(?:ast-[0-9a-f]{32})\z`).MatchString(assetID) { fail("Set ASSET_ID to the ID from the creation response") } bodyText = strings.ReplaceAll(bodyText, "\"asset://ast-bca04b1132034b9b42cb51fe29e9a0a7\"", strconv.Quote("asset://"+assetID)) request, err := http.NewRequest("POST", base+path, strings.NewReader(bodyText)) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) request.Header.Set("Content-Type", "application/json") idempotency := os.Getenv("IDEMPOTENCY_KEY") if idempotency == "" { idempotency = newOperationKey() } fmt.Fprintln(os.Stderr, "保存 IDEMPOTENCY_KEY,重试时原样复用:"+idempotency) request.Header.Set("Idempotency-Key", idempotency) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } // newOperationKey returns a random UUID; save it to retry with the same value. func newOperationKey() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { fail(err.Error()) } b[6] = b[6]&0x0f | 0x40 b[8] = b[8]&0x3f | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } ``` ### 参考视频和音频 - 参考视频用 `video_url` 项,`role` 写 `reference_video`;参考音频用 `audio_url` 项,`role` 写 `reference_audio`。 - 单段 2–15 秒,视频合计和音频合计都不超过 15 秒,最多 3 段视频和 3 段音频。 - 参考音频必须和参考图片或参考视频一起使用。 - 用 HTTPS 链接的参考视频按 15 秒输入预留费用;准备时按实际时长检查,总时长超限的任务以 `input_duration_exceeded` 失败,全额释放预留,不收费。 ### 素材来源 每个图片、视频或音频项可以用三种来源之一: | 来源 | 写法 | 说明 | | --- | --- | --- | | HTTPS 链接 | `https://…` | 平台先下载、检查并保存,再用于生成;这期间任务处于 `preparing_assets`。准备失败时全额释放预留,不收费。 | | 已登记的素材 | `asset://` 加素材 ID | 先[登记素材](./api-docs-zh-assets.md),等它变为 `active` 再引用。 | | base64 data URI | `data:image/png;base64,…` | 图片支持 JPEG、PNG、WebP,解码后小于 30 MB;音频支持 WAV、MP3,最大 15 MB、时长 2–15 秒;视频不支持。 | HTTPS 链接要用 443 端口,不能带用户信息、IP 地址或本地主机名,最长 2048 个字节。同一个请求里,HTTPS 链接和 base64 可以一起用,但都不能和 `asset://` 引用混用。 ### 声音、画幅与时长 - `generate_audio`:是否同时生成声音,默认 `false`。 - `ratio`:画幅,默认 `adaptive`,由模型按输入选择。 - `duration`:4–15 秒,默认 5 秒。 - `resolution`:默认 `720p`;Seedance 2.0 Fast 和 Mini 最高 `720p`。 - `watermark`、`seed`、`return_last_frame`:见「[视频模型](./api-docs-zh-video-models.md#outputs)」。 ### 大请求 请求体最大 64 MiB。大于 2 MiB 的请求体要在 120 秒内传完;每个团队同时处理的大请求有上限,满额时返回 `429 rate_limited`,稍后用同一个 `Idempotency-Key` 重试。 ### 下一步 - [任务状态与结果](./api-docs-zh-video-tasks.md):任务状态的含义、轮询和回调两种取结果方式、下载结果和取消任务。 - [上传素材](./api-docs-zh-assets.md):先登记素材,等它可用后在生成请求里用 asset:// 引用。 - [创建视频任务](./api-docs-zh-api-video-tasks-create.md):提交提示词和可选的图片、视频或音频,创建一个异步生成任务,立即返回任务 ID 和预留金额。 --- ## 任务状态与结果 任务状态的含义、轮询和回调两种取结果方式、下载结果和取消任务。 创建任务后,用任务 ID 查询进度和结果,或者等回调通知。 ### 任务状态 `queued` (排队中) → `processing` (处理中) → `succeeded` (已成功);另外两个终态 `failed` (已失败) 和 `cancelled` (已取消) 都不收费。 任务处于 `processing` 时,`phase` 说明当前阶段:准备素材、等待处理、准备生成、生成中、确认结果中、整理结果、等待结算。全部取值见「[状态与原因](./api-docs-zh-api-statuses.md#task-phase)」。 ### 三个状态字段 | 字段 | 说明 | | --- | --- | | `status` | 任务执行状态,终态是 `succeeded`、`failed` 和 `cancelled`。 | | `delivery_status` | 结果文件的交付进度,`available` 时可以下载。 | | `settlement_status` | 费用结算进度,`settled` 后费用确定。 | ### 轮询 每隔几秒查询一次,直到 `status` 变成终态: ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/video/tasks/${TASK_ID}" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} TASK_ID = os.environ["TASK_ID"] path = "/v1/video/tasks/{task_id}".replace("{task_id}", urllib.parse.quote(TASK_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const TASK_ID = process.env.TASK_ID; if (!TASK_ID) throw new Error("Set TASK_ID"); const path = "/v1/video/tasks/{task_id}".replace("{task_id}", encodeURIComponent(TASK_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } taskID := os.Getenv("TASK_ID") if taskID == "" { fail("Set TASK_ID") } path := strings.Replace("/v1/video/tasks/{task_id}", "{task_id}", url.PathEscape(taskID), 1) request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` ### 回调 在控制台登记并验证回调地址后,创建任务时传 `callback_url`,任务结束会收到 `task.completed`。通知不含下载地址,收到后再查询一次任务。详见「[接收回调](./api-docs-zh-callbacks.md)」。 ### 下载结果 成功的任务在 `output.assets` 里列出结果文件,`role` 为 `result`。`url` 是 HTTPS 下载地址,每次查询任务都会重新签发,最长 24 小时有效,且不晚于 `expires_at`(默认结果准备好后 7 天);过期或暂不可用时 `url` 为空,`download_status` 说明原因。请在有效期内下载到自己的存储。 ```bash curl --output video.mp4 "$VIDEO_URL" ``` ### 列出任务 `GET /v1/video/tasks` 列出团队的全部任务,不限于创建任务的那一把 API 密钥,按创建时间从新到旧排列。列表只含标识、模型、状态和时间;用量、费用、结果和错误请按任务 ID 查询。 ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/video/tasks" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} path = "/v1/video/tasks" request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const path = "/v1/video/tasks"; const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } path := "/v1/video/tasks" request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` ### 取消任务 `POST /v1/video/tasks/{task_id}/cancel` 需要 `Idempotency-Key`,请求体为空。 - 生成开始前取消立即生效:返回 `effective`,任务变为 `cancelled`,预留全额释放。 - 生成开始后返回 `not_effective`,原因码 `generation_already_started`,任务继续生成并照常结算。 - 已经结束、又没有取消操作的任务返回 `409 task_not_cancellable`。 ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} printf '%s\n' "保存 IDEMPOTENCY_KEY,重试时原样复用:${IDEMPOTENCY_KEY}" >&2 curl --request POST "${CHENVOYA_BASE_URL}/v1/video/tasks/${TASK_ID}/cancel" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" \ --header "Idempotency-Key: ${IDEMPOTENCY_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} idempotency = os.environ.get("IDEMPOTENCY_KEY") or str(uuid.uuid4()) print("保存 IDEMPOTENCY_KEY,重试时原样复用:" + idempotency, file=sys.stderr) headers["Idempotency-Key"] = idempotency TASK_ID = os.environ["TASK_ID"] path = "/v1/video/tasks/{task_id}/cancel".replace("{task_id}", urllib.parse.quote(TASK_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="POST") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const idempotency = process.env.IDEMPOTENCY_KEY || crypto.randomUUID(); console.error("保存 IDEMPOTENCY_KEY,重试时原样复用:" + idempotency); headers["Idempotency-Key"] = idempotency; const TASK_ID = process.env.TASK_ID; if (!TASK_ID) throw new Error("Set TASK_ID"); const path = "/v1/video/tasks/{task_id}/cancel".replace("{task_id}", encodeURIComponent(TASK_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "POST", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "crypto/rand" "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } taskID := os.Getenv("TASK_ID") if taskID == "" { fail("Set TASK_ID") } path := strings.Replace("/v1/video/tasks/{task_id}/cancel", "{task_id}", url.PathEscape(taskID), 1) request, err := http.NewRequest("POST", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) idempotency := os.Getenv("IDEMPOTENCY_KEY") if idempotency == "" { idempotency = newOperationKey() } fmt.Fprintln(os.Stderr, "保存 IDEMPOTENCY_KEY,重试时原样复用:"+idempotency) request.Header.Set("Idempotency-Key", idempotency) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } // newOperationKey returns a random UUID; save it to retry with the same value. func newOperationKey() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { fail(err.Error()) } b[6] = b[6]&0x0f | 0x40 b[8] = b[8]&0x3f | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } ``` ### 失败原因 失败的任务在 `error.code` 里给出原因,例如素材下载失败的 `input_fetch_failed`、生成失败的 `generation_failed`。全部取值见「[状态与原因](./api-docs-zh-api-statuses.md#task-error)」。失败或取消的任务结算后费用为零。 ### 下一步 - [接收回调](./api-docs-zh-callbacks.md):登记回调地址后,任务结束和费用结算时我们向它发送签名的通知。 - [计费方式](./api-docs-zh-billing.md):创建时按上限预留,完成后按实际用量结算,失败和取消的任务不收费。 - [查询视频任务](./api-docs-zh-api-video-tasks-retrieve.md):读取任务当前状态、交付情况、用量与结算摘要。 --- ## 上传素材 先登记素材,等它可用后在生成请求里用 asset:// 引用。 素材是登记在平台上的图片、视频或音频。登记一次后,可以在多个生成请求里重复引用。 ### 什么时候用素材 生成请求里的图片、视频和音频有三种来源:直接写 HTTPS 链接、写 base64 data URI,或者引用已登记的素材。同一份文件要反复使用时,登记成素材更省事;只用一次时直接写链接即可。三种来源的规则见「[生成视频](./api-docs-zh-video-generate.md#sources)」。 ### 登记素材 向 `POST /v1/assets` 发送公开 HTTPS 地址、媒体类型和模型。登记需要 `Idempotency-Key`,API 密钥需要 `service.use` 权限。 ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} printf '%s\n' "保存 IDEMPOTENCY_KEY,重试时原样复用:${IDEMPOTENCY_KEY}" >&2 curl --request POST "${CHENVOYA_BASE_URL}/v1/assets" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" \ --header "Idempotency-Key: ${IDEMPOTENCY_KEY}" \ --header 'Content-Type: application/json' \ --data '{ "url": "https://images.example.test/reference.png", "media_type": "image", "model": "dreamina-seedance-2-0-260128", "name": "reference" }' ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} idempotency = os.environ.get("IDEMPOTENCY_KEY") or str(uuid.uuid4()) print("保存 IDEMPOTENCY_KEY,重试时原样复用:" + idempotency, file=sys.stderr) headers["Idempotency-Key"] = idempotency path = "/v1/assets" body_text = "{\"url\":\"https://images.example.test/reference.png\",\"media_type\":\"image\",\"model\":\"dreamina-seedance-2-0-260128\",\"name\":\"reference\"}" headers["Content-Type"] = "application/json" body = json.dumps(json.loads(body_text)).encode("utf-8") request = urllib.request.Request(base + path, data=body, headers=headers, method="POST") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const idempotency = process.env.IDEMPOTENCY_KEY || crypto.randomUUID(); console.error("保存 IDEMPOTENCY_KEY,重试时原样复用:" + idempotency); headers["Idempotency-Key"] = idempotency; const path = "/v1/assets"; let bodyText = "{\"url\":\"https://images.example.test/reference.png\",\"media_type\":\"image\",\"model\":\"dreamina-seedance-2-0-260128\",\"name\":\"reference\"}"; headers["Content-Type"] = "application/json"; const body = JSON.stringify(JSON.parse(bodyText)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "POST", headers, body: body}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "crypto/rand" "fmt" "io" "net/http" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } path := "/v1/assets" bodyText := "{\"url\":\"https://images.example.test/reference.png\",\"media_type\":\"image\",\"model\":\"dreamina-seedance-2-0-260128\",\"name\":\"reference\"}" request, err := http.NewRequest("POST", base+path, strings.NewReader(bodyText)) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) request.Header.Set("Content-Type", "application/json") idempotency := os.Getenv("IDEMPOTENCY_KEY") if idempotency == "" { idempotency = newOperationKey() } fmt.Fprintln(os.Stderr, "保存 IDEMPOTENCY_KEY,重试时原样复用:"+idempotency) request.Header.Set("Idempotency-Key", idempotency) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } // newOperationKey returns a random UUID; save it to retry with the same value. func newOperationKey() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { fail(err.Error()) } b[6] = b[6]&0x0f | 0x40 b[8] = b[8]&0x3f | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } ``` 接受后平台在后台下载并校验,响应头 `Location` 是查询路径。 ```json { "asset_id": "ast-11111111111111111111111111111111", "status": "processing" } ``` ### 查询状态 用 `GET /v1/assets/{asset_id}` 查询,直到状态不再是 `processing`: | 状态 | 含义 | | --- | --- | | `processing` | 正在下载和校验,还不能引用。 | | `active` | 可以引用;同时返回大小、摘要,以及图片和视频的尺寸、音视频的时长。 | | `unavailable` | 不可用,`failure_reason` 说明原因。 | ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/assets/${ASSET_ID}" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} ASSET_ID = os.environ["ASSET_ID"] if not re.fullmatch("^ast-[0-9a-f]{32}$", ASSET_ID): sys.exit("Set ASSET_ID to the ID from the creation response") path = "/v1/assets/{asset_id}".replace("{asset_id}", urllib.parse.quote(ASSET_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const ASSET_ID = process.env.ASSET_ID; if (!ASSET_ID) throw new Error("Set ASSET_ID"); if (typeof ASSET_ID !== "string" || (new RegExp("^ast-[0-9a-f]{32}$")).exec(ASSET_ID)?.[0] !== ASSET_ID) throw new Error("Set ASSET_ID to the ID from the creation response"); const path = "/v1/assets/{asset_id}".replace("{asset_id}", encodeURIComponent(ASSET_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } assetID := os.Getenv("ASSET_ID") if assetID == "" { fail("Set ASSET_ID") } if !regexp.MustCompile(`\A(?:ast-[0-9a-f]{32})\z`).MatchString(assetID) { fail("Set ASSET_ID to the ID from the creation response") } path := strings.Replace("/v1/assets/{asset_id}", "{asset_id}", url.PathEscape(assetID), 1) request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` ### 在请求里引用 在 `image_url`、`video_url` 或 `audio_url` 的 `url` 里写 `asset://` 加素材 ID。为一个模型登记的素材,也可以用在兼容的其他模型的请求里。 - 引用仍在处理中的素材返回 `409 asset_not_ready`,等它变为 `active` 后用同一个 `Idempotency-Key` 重试。 - 素材不属于本团队、不存在或已删除时,统一返回 `asset_not_found`。 - 同一个请求里不能把 `asset://` 引用和 HTTPS 链接或 base64 混用。 ### 地址和额度 - 来源地址必须是公开的 HTTPS 地址,最长 2048 个字节,不能带用户信息;不安全的目标地址返回 `400 source_url_not_allowed`。 - 团队的素材存储额度用完时返回 `429 asset_storage_quota_exceeded`;接入容量受限时返回 `429 asset_intake_rate_limit_exceeded`,稍后重试。 ### 下一步 - [生成视频](./api-docs-zh-video-generate.md):文生视频、首帧、首尾帧、参考图和参考音视频的请求写法。 - [登记素材](./api-docs-zh-api-assets-create.md):需要 service.use 权限。 - [素材对象](./api-docs-zh-api-assets-object.md):素材状态与安全元数据。 --- ## 接收回调 登记回调地址后,任务结束和费用结算时我们向它发送签名的通知。 回调让你不用一直轮询:任务结束时我们向你登记的 HTTPS 地址发送 `POST` 请求。 ### 登记地址 1. 在控制台的「[回调](./callbacks.html)」页添加 HTTPS 地址并完成验证,保存登记时显示的签名密钥 `signing_secret`。 2. 创建任务时在请求体里写 `callback_url`,值必须与本团队已验证、未撤销且未过期的地址逐字节相同,否则返回 `400 callback_target_not_verified`。 3. 收到通知时先[验证签名](./api-docs-zh-callbacks-verify.md),再处理正文。 ### 两种事件 | 事件 | 什么时候发送 | | --- | --- | | [`task.completed`](./api-docs-zh-api-events-task-completed.md) | 任务成功、失败或取消之后;成功的任务要先完成交付处理。 | | [`task.billing_settled`](./api-docs-zh-api-events-task-billing-settled.md) | `task.completed` 已送达或放弃、且费用结算确定之后。完成后三十天仍未结算的任务不发送。 | 两个事件的正文都是任务快照,不含结果下载地址;需要下载时再查询任务。 ### 响应与重试 只有 2xx 响应表示已收到。其他状态或超时会重试:第一次失败后依次等待 10 秒、1 分钟、5 分钟,之后每小时一次,每个事件最长 7 天。`task.billing_settled` 的 7 天从它自己开始投递时算起。 ### 去重 每个事件至少送达一次,可能重复。验证签名后,按正文里的 `event_id` 去重;请求头 `Chenvoya-Event-Id` 没有被签名,只是方便查看的副本。处理过的事件 ID 至少保存 7 天零 5 分钟。重试时事件 ID 和正文的原始字节都不变。 ### 撤销地址 在控制台撤销地址后不再开始新的投递,但已经开始发送的那一次仍可能到达。控制台可以查看通知记录;任务的当前状态和下载结果始终以查询任务为准。 ### 下一步 - [验证回调签名](./api-docs-zh-callbacks-verify.md):用登记地址时拿到的签名密钥,验证每个通知确实来自我们。 - [接收任务完成通知](./api-docs-zh-api-events-task-completed.md):任务进入成功、失败或取消终态后发送 task.completed;成功任务须已完成交付处理。 - [接收任务结算通知](./api-docs-zh-api-events-task-billing-settled.md):task.completed 已送达或放弃且任务结算为 settled 或 settled_with_receivable 后,发送 task.billing_settled,使用独立的七天投递窗口。 --- ## 验证回调签名 用登记地址时拿到的签名密钥,验证每个通知确实来自我们。 每个通知都带 `Chenvoya-Signature` 请求头。先验证签名,再读取正文;验证失败时返回非 2xx 状态并丢弃这次请求。 ### 签名格式 ```text Chenvoya-Signature: t=1793167400,v1=<64 位小写十六进制> ``` `t` 是签名时间(Unix 秒),`v1` 是小写十六进制的 HMAC-SHA256。 ### 验证步骤 1. 把登记时取得的 `signing_secret` 做 base64url 解码,得到 32 字节的验签密钥。 2. 从 `Chenvoya-Signature` 拆出 `t` 和 `v1`。 3. `t` 与本地时间相差超过 5 分钟时拒绝。 4. 用验签密钥计算 HMAC-SHA256,输入是 `chenvoya.v1.`、`t`、一个点 `.`,再接正文的原始字节。 5. 把结果写成小写十六进制,与 `v1` 做常量时间比较。 6. 通过后按正文的 `event_id` 去重;`created_at` 早于 7 天零 5 分钟前或晚于现在 5 分钟以上的事件也要拒绝。 ### 示例代码 ```python import base64 import hashlib import hmac import time def verify(signing_secret, header, body): fields = {} for item in header.split(","): name, _, value = item.strip().partition("=") fields[name] = value t, v1 = fields.get("t", ""), fields.get("v1", "") if not t.isdigit() or abs(time.time() - int(t)) > 300: return False key = base64.urlsafe_b64decode(signing_secret + "=" * (-len(signing_secret) % 4)) signed = b"chenvoya.v1." + t.encode() + b"." + body expected = hmac.new(key, signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, v1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 import { createHmac, timingSafeEqual } from "node:crypto"; export function verify(signingSecret, header, body) { const fields = {}; for (const item of header.split(",")) { const at = item.indexOf("="); if (at > 0) fields[item.slice(0, at).trim()] = item.slice(at + 1); } const t = fields.t ?? "", v1 = fields.v1 ?? ""; if (!/^[0-9]+$/.test(t) || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; const key = Buffer.from(signingSecret, "base64url"); const expected = createHmac("sha256", key).update("chenvoya.v1." + t + ".").update(body).digest("hex"); return v1.length === expected.length && timingSafeEqual(Buffer.from(expected), Buffer.from(v1)); } ``` ```go package callback import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "strconv" "strings" "time" ) // Verify reports whether body carries a valid Chenvoya-Signature header. func Verify(signingSecret, header string, body []byte) bool { fields := map[string]string{} for _, item := range strings.Split(header, ",") { name, value, _ := strings.Cut(strings.TrimSpace(item), "=") fields[name] = value } t, err := strconv.ParseInt(fields["t"], 10, 64) if err != nil || t < 0 || time.Since(time.Unix(t, 0)).Abs() > 5*time.Minute { return false } key, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(signingSecret, "=")) if err != nil { return false } mac := hmac.New(sha256.New, key) mac.Write([]byte("chenvoya.v1." + fields["t"] + ".")) mac.Write(body) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(fields["v1"])) } ``` ### 检查清单 - 用收到的原始字节计算签名,不要先解析 JSON 再重新编码。 - 用常量时间比较,不要用普通的字符串相等。 - 服务器时钟要准确,否则 5 分钟的时间窗口会误拒。 - 按 `event_id` 去重,处理过的事件 ID 至少保存 7 天零 5 分钟。 - 签名密钥和 API 密钥一样只放在服务器上。 ### 下一步 - [接收回调](./api-docs-zh-callbacks.md):登记回调地址后,任务结束和费用结算时我们向它发送签名的通知。 - [接收任务完成通知](./api-docs-zh-api-events-task-completed.md):任务进入成功、失败或取消终态后发送 task.completed;成功任务须已完成交付处理。 --- ## 计费方式 创建时按上限预留,完成后按实际用量结算,失败和取消的任务不收费。 视频生成按用量计费,用量记录在 `video_tokens` 等计量字段里。每个任务的费用都经过预留、结算两步。 ### 预留 创建任务时,平台按这次请求可能产生的最大用量(`quote.estimated_usage_upper_bound.video_tokens`)和当前价格版本算出预留金额 `quote.reserved_amount`,从余额里预留出来。余额不够时返回 `429 insufficient_balance`,`error.details` 给出所需的预留金额,任务不会创建。 ### 结算 任务完成后按实际用量(`usage.video_tokens`)结算:实际费用从预留里扣除,多出的部分释放回余额。`settlement_status` 变为 `settled` 后,`billing.final_actual_cost` 就是这个任务的最终费用。 ### 失败和取消不收费 - 失败或取消的任务结算后,最终费用和应收金额都是零,预留全额释放。 - 用 HTTPS 链接的素材准备失败时,任务以 `input_*` 错误失败,预留全额释放,不收生成费。 - 被拒绝的请求(4xx)发生在预留之前,不会预留也不会收费。 ### 一个例子 下面的数字来自本文档的示例任务: | 步骤 | 用量 | 金额(美元) | | --- | --- | --- | | 创建时预留 | 上限 111779 个 `video_tokens` | `0.782453` | | 完成后扣费 | 实际 51652 个 `video_tokens` | `0.361564` | | 释放差额 | — | `0.420889` | ### 金额格式 所有金额都是美元,写成六位小数的字符串,例如 `"0.361564"`。尚未确定的金额为 `null`。 ### 价格 各模型的标价见「[价格](./public-prices.html)」页,本团队适用的价格和版本在控制台查看。文档不写死价格。 ### 下一步 - [对账](./api-docs-zh-billing-reconcile.md):用任务账单接口核对每个任务的预留、扣款、退款和应收。 - [查询单任务账单](./api-docs-zh-api-video-tasks-billing.md):读取本团队任务的稳定账单汇总及一页有序、不可变的账单事件。 --- ## 对账 用任务账单接口核对每个任务的预留、扣款、退款和应收。 每个任务都有一份账单:一个汇总加一串不可变的账单事件。汇总回答「这个任务一共收了多少」,事件回答「钱是怎么动的」。 ### 读取账单 ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/video/tasks/${TASK_ID}/billing" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} TASK_ID = os.environ["TASK_ID"] path = "/v1/video/tasks/{task_id}/billing".replace("{task_id}", urllib.parse.quote(TASK_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const TASK_ID = process.env.TASK_ID; if (!TASK_ID) throw new Error("Set TASK_ID"); const path = "/v1/video/tasks/{task_id}/billing".replace("{task_id}", encodeURIComponent(TASK_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } taskID := os.Getenv("TASK_ID") if taskID == "" { fail("Set TASK_ID") } path := strings.Replace("/v1/video/tasks/{task_id}/billing", "{task_id}", url.PathEscape(taskID), 1) request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` 汇总属于整个任务,不会因为事件分页变成当前页的小计。事件按 `cursor` 和 `page_size` 翻页,最后一页的 `next_cursor` 为 `null`。 ### 汇总字段 | 字段 | 含义 | | --- | --- | | `reserved_amount` | 任务预留的金额。 | | `charged_amount` | 任务的计费金额。 | | `funding_debit_amount` | 实际从余额扣减的金额。 | | `funding_refund_amount` | 实际退回余额的金额。 | | `outstanding_receivable` | 尚未收取的应收金额。 | | `final_actual_cost` | 最终实际费用;结算确定前为 `null`。 | ### 账单事件 每次资金变动都记一条事件,例如创建预留的 `hold_created`、收取费用的 `charge_captured`、释放预留的 `hold_released`。预留的创建、增加、减少和释放本身不表示扣款、退款或应收变动,金额以各金额字段为准。全部事件类型见「[状态与原因](./api-docs-zh-api-statuses.md#billing-event)」。 ### 需要核对时 少数任务的用量需要核对,这时 `settlement_status` 为 `reconciliation_required`,`final_actual_cost` 为 `null`,`reconciliation` 给出核对原因 `reason_code`、缺少的计量字段 `missing_meters`,以及是否需要你操作的 `customer_action_required`。核对完成后结算状态变为 `settled` 或 `settled_with_receivable`。 ### 关联标识 `request_id` 和 `billing_correlation_id` 在任务、账单和回调里保持一致,可以用来把你的订单和我们的账单对上。 ### 下一步 - [计费方式](./api-docs-zh-billing.md):创建时按上限预留,完成后按实际用量结算,失败和取消的任务不收费。 - [查询单任务账单](./api-docs-zh-api-video-tasks-billing.md):读取本团队任务的稳定账单汇总及一页有序、不可变的账单事件。 - [状态与原因](./api-docs-zh-api-statuses.md):任务、交付、结算、素材和账单的全部取值。 --- ## 幂等与重试 用同一个 Idempotency-Key 重试,不会重复创建任务,也不会重复扣费。 网络会超时,响应会丢失。幂等键让你在「不知道上一次成没成功」时可以放心重发。 ### 哪些请求要带 创建视频任务、取消视频任务和登记素材这三个操作必须带请求头 `Idempotency-Key`。 ### 怎么生成 - 每个新操作生成一个新值,例如 UUID,在发送前保存下来。 - 长度 8–128 个字符,以字母或数字开头,只用字母、数字和 `.`、`_`、`:`、`-`。 - 唯一性按团队、操作和键确定,不按 API 密钥区分:同一团队的两把 API 密钥用同一个键也会被当成同一个操作。 ### 结果不明时 请求超时、连接断开或收到 `500`、`503` 时,用原请求体和同一个已保存的键重发。已经成功的操作会返回原响应,不会再创建一次,也不会再扣一次费。重试时绝不生成新值。 ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} ``` 示例代码都这样处理:环境变量里已有 `IDEMPOTENCY_KEY` 就复用,没有才生成一次,并提示你保存。 ### 冲突 | 状态码 | error.code | 含义 | | --- | --- | --- | | 409 | `idempotency_conflict` | 同一个键已经用于另一个请求。重试原请求,或给新请求换一个新键。 | | 409 | `idempotency_replay_expired` | 原操作的重放响应已过期,原操作不会再执行一次。按 ID 查询原对象。 | ### 取消的幂等 取消操作永久保留它的原响应。对同一个任务用新的键再次取消,会引用同一个取消操作,并返回当时冻结的任务状态。 ### 下一步 - [处理错误](./api-docs-zh-errors.md):错误响应的格式,以及哪些错误可以原样重试。 - [限流与额度](./api-docs-zh-limits.md):请求频率、大请求并发、余额和素材额度都可能让新请求被拒绝。 --- ## 处理错误 错误响应的格式,以及哪些错误可以原样重试。 接口用 HTTP 状态码表示结果,出错时响应体里有机器可读的错误码。按错误码写处理逻辑,不要解析说明文字。 ### 错误格式 错误响应包含 `error` 对象和 `trace_id`: | 字段 | 说明 | | --- | --- | | `error.code` | 机器可读的错误码,按它处理错误。 | | `error.message` | 供人阅读的说明,可能调整,不要拿来判断。 | | `error.details` | 可选的结构化详情,例如余额不足时所需的预留金额。 | | `trace_id` | 排查问题用的追踪标识,联系我们时请提供。 | ### 能否重试 | 情况 | 怎么做 | | --- | --- | | `400`、`401`、`403`、`404` | 请求本身有问题,原样重试还会失败。按错误码修正后再发送。 | | `409` | 多数是幂等键冲突,见「[幂等与重试](./api-docs-zh-idempotency.md#conflict)」;`asset_not_ready` 等素材可用后用同一个键重试。 | | `429` | 频率、余额或额度受限。有 `Retry-After` 时等这么多秒,再用同一个键重试;余额不足先充值。 | | `500`、`503` | 结果不确定。用原请求体和同一个 `Idempotency-Key` 重试,不会重复创建或扣费。 | | 超时、连接断开 | 同上,用同一个键原样重试。 | 每个错误码的含义、处理办法和是否收费见「[错误码](./api-docs-zh-api-errors.md)」。 ### 细节字段 - `unsupported_capability`:`error.details` 列出不能同时满足的能力和相关的请求参数名,按参数调整请求。 - `insufficient_balance`:`error.details` 给出这次请求所需的预留金额和价格版本。 ### 任务失败 接口接受请求后,任务仍可能在后台失败。失败的任务 `status` 为 `failed`,`error.code` 给出原因,例如 `input_fetch_failed` 或 `generation_failed`,全部取值见「[状态与原因](./api-docs-zh-api-statuses.md#task-error)」。失败的任务结算后不收费。 ### 下一步 - [错误码](./api-docs-zh-api-errors.md):每个 error.code 的含义、状态码和出现的接口。 - [幂等与重试](./api-docs-zh-idempotency.md):用同一个 Idempotency-Key 重试,不会重复创建任务,也不会重复扣费。 --- ## 限流与额度 请求频率、大请求并发、余额和素材额度都可能让新请求被拒绝。 请求频率、素材接入容量、存储额度、API 密钥额度或余额都可能拒绝新请求。按错误码处理这些限制;文档不写死具体数字。 ### 频率限制 请求太频繁时返回 `429 rate_limited`。响应可能带 `Retry-After`(1–3600 秒),等这么多秒后用同一个 `Idempotency-Key` 重试。 ### 大请求并发 请求体大于 2 MiB 的创建请求,每个团队同时处理的数量有上限;满额时返回 `429 rate_limited`,这时不带 `Retry-After`,稍后重试即可。大请求体要在 120 秒内传完。 ### 余额和 API 密钥额度 余额不够预留时返回 `429 insufficient_balance`,`error.details` 给出所需的预留金额;充值后用同一个键重试。API 密钥设有额度时,用完后新请求同样会被拒绝,按返回的错误码处理。 ### 素材额度 - `429 asset_storage_quota_exceeded`:团队的素材存储额度已用完,`Retry-After` 不适用。 - `429 asset_intake_rate_limit_exceeded`:素材接入容量受限,稍后重试。 ### 暂停接入 `503 funding_intake_closed` 表示暂时不接受新任务;已经接受的请求原样重放仍返回原响应。稍后再试。 ### 下一步 - [处理错误](./api-docs-zh-errors.md):错误响应的格式,以及哪些错误可以原样重试。 - [幂等与重试](./api-docs-zh-idempotency.md):用同一个 Idempotency-Key 重试,不会重复创建任务,也不会重复扣费。 --- ## 常见问题 计费、失败、结果保存时间和 API 密钥的常见问题。 没有找到答案时,带上 `trace_id` 或任务 ID 联系我们。 ### 计费 #### 任务失败会收费吗? 不会。失败或取消的任务结算后,最终费用和应收金额都是零,预留全额释放。详见「[计费方式](./api-docs-zh-billing.md#free)」。 #### 为什么创建时预留的金额比最后的费用多? 创建时按这次请求可能产生的最大用量预留,完成后按实际用量结算,多出的部分释放回余额。 ### 任务 #### 请求超时了,要重新发吗? 要,用原请求体和同一个 `Idempotency-Key` 重发。如果上一次其实已经成功,会返回原来的任务,不会重复创建或扣费。详见「[幂等与重试](./api-docs-zh-idempotency.md#retry)」。 #### 能取消正在生成的任务吗? 只能在生成开始前取消。生成开始后取消返回 `not_effective`,任务会继续生成并照常结算。 #### 结果能保存多久? 结果保存到 `expires_at`,默认是结果准备好后 7 天。每次查询任务都会返回新的 `url`,最长 24 小时有效,且不晚于 `expires_at`。请在有效期内下载到自己的存储,过期后 `url` 为空。 #### 为什么查不到某个任务? 任务按团队隔离。任务 ID 不存在或属于其他团队时都返回 `404 task_not_found`。 ### API 密钥和回调 #### API 密钥泄露了怎么办? 立刻在控制台的「[API 密钥](./integration.html)」页停用它,新建一把并替换服务器上的值。 #### 没收到回调怎么办? 先确认回调地址在控制台已验证且没有撤销,创建任务时 `callback_url` 与登记的地址逐字节相同。失败的投递会重试 7 天,控制台可以查看通知记录。无论回调是否到达,任务的状态和结果都以查询任务为准。 ### 下一步 - [处理错误](./api-docs-zh-errors.md):错误响应的格式,以及哪些错误可以原样重试。 - [计费方式](./api-docs-zh-billing.md):创建时按上限预留,完成后按实际用量结算,失败和取消的任务不收费。 --- ## OpenAPI 与 Markdown 下载 API 定义、给 AI 助手的页面索引和每一页的 Markdown。 文档的内容也有机器可读的版本,方便生成客户端代码或交给 AI 助手。 ### API 定义 [api-openapi.json](./api-openapi.json) 是 OpenAPI 3.1 格式的 API 定义,只包含客户接口和回调事件,可以导入接口调试工具或代码生成器。 ### llms.txt [api-llms.txt](./api-llms.txt) 列出每一页英文文档的 Markdown 地址和一句话说明,适合交给 AI 助手作为入口。 ### Markdown - 每一页顶部都有「复制 Markdown」和「查看 Markdown」,内容与网页相同。 - 全部中文文档合成一个文件:[api-docs.md](./api-docs.md)。 - 全部英文文档合成一个文件:[api-docs-en.md](./api-docs-en.md)。 ### 下一步 - [快速开始](./api-docs-zh-quickstart.md):三步生成第一个视频:创建 API 密钥,提交任务,取回结果。 - [API 概览](./api-docs-zh-api.md):接入地址、认证、错误格式和全部接口。 --- ## API 概览 接入地址、认证、错误格式和全部接口。 **接入地址**:向开户人员索取接入地址。 ### 认证 客户接口使用 Bearer 方案的 API Key,通过请求头传递。本地将值保存在 CHENVOYA_API_KEY 环境变量,示例仅引用变量名。不要将实际值粘贴进文档或日志。 ```bash Authorization: Bearer ${CHENVOYA_API_KEY} ``` ### 错误格式 错误响应包含 error 对象和 trace_id;error.code 是机器可读错误码,message 是说明,可选 details 提供结构化详情。下文按各操作合同列出错误与状态,不根据说明推断额外重试或收费承诺。 全部错误码及处理办法见「[错误码](./api-docs-zh-api-errors.md)」。 ### 全部接口 | 方法 | 路径 | 说明 | | --- | --- | --- | | POST | `/v1/video/generations` | [创建视频任务](./api-docs-zh-api-video-tasks-create.md) | | GET | `/v1/video/tasks/{task_id}` | [查询视频任务](./api-docs-zh-api-video-tasks-retrieve.md) | | GET | `/v1/video/tasks` | [列出团队任务](./api-docs-zh-api-video-tasks-list.md) | | POST | `/v1/video/tasks/{task_id}/cancel` | [取消视频任务](./api-docs-zh-api-video-tasks-cancel.md) | | GET | `/v1/video/tasks/{task_id}/billing` | [查询单任务账单](./api-docs-zh-api-video-tasks-billing.md) | | POST | `/v1/assets` | [登记素材](./api-docs-zh-api-assets-create.md) | | GET | `/v1/assets/{asset_id}` | [查询素材状态](./api-docs-zh-api-assets-retrieve.md) | | 回调 | `task.completed` | [接收任务完成通知](./api-docs-zh-api-events-task-completed.md) | | 回调 | `task.billing_settled` | [接收任务结算通知](./api-docs-zh-api-events-task-billing-settled.md) | --- ## 视频任务对象 视频任务详情。结果不可用时任务保持处理中、结算需核对;成功任务有结果素材,且交付已可用。失败或取消的任务结算可为待处理或已结算;已结算的失败或取消任务最终费用及未收款为零。 ### 属性 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `id` | string | 必填 | 平台任务标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `object` | string | 必填 | 对象类型标识。 固定为:video.task。 | | `request_id` | string | 必填 | 请求关联标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `billing_correlation_id` | string | 必填 | 费用关联标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `model` | string | 必填 | 本次使用的模型标识。 `dreamina-seedance-2-0-260128`:Seedance 2.0; `dreamina-seedance-2-0-fast-260128`:Seedance 2.0 Fast; `dreamina-seedance-2-0-mini-260615`:Seedance 2.0 Mini; | | `status` | string | 必填 | 当前状态,具体含义见对应取值。 `queued`:排队中; `processing`:处理中; `succeeded`:已成功; `failed`:已失败; `cancelled`:已取消; | | `phase` | string | 必填,可能为空 | 任务执行阶段;没有当前阶段时为空。 `preparing_assets`:准备素材; `waiting_route`:等待处理; `submitting`:准备生成; `provider_processing`:生成中; `reconciling`:确认结果中; `finalizing`:整理结果; `settlement_pending`:等待结算; `null`:空值; | | `delivery_status` | string | 必填 | 结果交付状态。 `pending`:待交付; `materializing`:准备下载; `available`:可下载; `output_unavailable`:结果不可用; | | `settlement_status` | string | 必填 | 当前费用结算状态。 `reserved`:已预留费用; `pending`:待处理; `reconciliation_required`:需要核对; `settled`:已结算; `settled_with_receivable`:已结算,仍有应收金额; | | `created_at` | integer | 必填 | 创建时间,Unix 整数秒。 范围:≥ 0。 | | `started_at` | integer | 必填,可能为空 | 任务开始时间,未开始时为空。 范围:≥ 0。 | | `completed_at` | integer | 必填,可能为空 | 任务完成时间,未完成时为空。 范围:≥ 0。 | | `usage` | object | 必填,可能为空 | 任务用量对象,可为空。 | | `usage.meter_type` | string | 必填 | 用量计量类型。 固定为:seedance_video_generation。 | | `usage.video_tokens` | integer | 必填,可能为空 | 视频计量单位数;实际用量允许为空的情况以字段类型为准。 范围:≥ 0。 | | `usage.output_duration_seconds` | integer | 必填 | 输出视频时长,秒。 范围:≥ 0。 | | `usage.input_video_duration_seconds` | integer | 必填 | 输入参考视频总时长,秒。 范围:≥ 0。 | | `usage.resolution` | string | 必填 | 输出分辨率。fast 和 mini 仅支持 480p 或 720p。 `480p`:四百八十线分辨率; `720p`:七百二十线分辨率; `1080p`:一千零八十线分辨率; | | `usage.fps` | integer | 必填 | 视频帧率。 范围:1–120。 | | `usage.has_reference_video` | boolean | 必填 | 是否使用参考视频。 | | `billing` | object | 必填 | 任务结算摘要,可为空。 | | `billing.currency` | string | 必填 | 金额币种,美元。 固定为:USD。 | | `billing.final_actual_cost` | string | 必填,可能为空 | 最终实际费用;尚未确定时为空。 格式:6 位小数。 | | `billing.collected_liability_amount` | string | 必填 | 已收取的计费金额。 格式:6 位小数。 | | `billing.funding_debit_amount` | string | 必填 | 实际扣减的资金金额。 格式:6 位小数。 | | `billing.refunded_liability_amount` | string | 必填 | 已退回的计费金额。 格式:6 位小数。 | | `billing.funding_refund_amount` | string | 必填 | 实际退回的资金金额。 格式:6 位小数。 | | `billing.outstanding_receivable` | string | 必填 | 尚未收取的应收金额。 格式:6 位小数。 | | `billing.billing_url` | string | 必填 | 本任务的费用明细相对路径。 格式:/v1/video/tasks/{task_id}/billing。 | | `output` | object | 必填,可能为空 | 任务生成结果,可为空。 | | `output.assets` | array | 必填 | 结果素材列表。 数量:1–16。 | | `output.assets.asset_id` | string | 必填 | 平台素材标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `output.assets.media_type` | string | 必填 | 媒体类型。 `video`:视频; `image`:图片; `audio`:音频; | | `output.assets.role` | string | 必填 | 素材在输入或结果中的用途。 `result`:生成结果; `last_frame`:尾帧; `audio`:音频; | | `output.assets.url` | string | 必填,可能为空 | 可下载时为 HTTPS 下载地址;过期或不可用时为空。 长度:12–4096。 格式:https://。 | | `output.assets.materialized_at` | integer | 必填,可能为空 | 结果准备完成的时间,Unix 整数秒。 范围:≥ 0。 | | `output.assets.expires_at` | integer | 必填,可能为空 | 结果到期时间,为结果准备完成后再过当时生效的保留期(默认 7 天),与任务完成时间分别计算。 范围:≥ 0。 | | `output.assets.delivery_source` | string | 必填 | 结果交付来源。 `platform`:平台交付; | | `output.assets.materialization_status` | string | 必填 | 结果准备状态。 `completed`:已完成; `retrying`:重试中; `failed`:失败; | | `output.assets.download_status` | string | 必填 | 下载状态:可用、过期或不可用。 `available`:可用; `expired`:已过期; `unavailable`:不可用; | | `cancellation` | object | 必填,可能为空 | 取消状态投影,可为空。 | | `cancellation.cancel_operation_id` | string | 必填 | 取消操作的稳定标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `cancellation.status` | string | 必填 | 当前状态,具体含义见对应取值。 `requested`:已请求取消; `effective`:取消已生效; `not_effective`:取消未生效; `failed`:失败; | | `cancellation.reason_code` | string | 必填,可能为空 | 取消未生效或失败时的原因码;已请求或已生效时为空。 长度:1–96。 字符:a–z 0–9 _。 | | `error` | object | 必填,可能为空 | 错误对象。 | | `error.code` | string | 必填 | 机器可读错误码。 `no_common_asset_route`:素材暂不可用; `asset_not_ready`:引用素材仍在处理中; `asset_review_timeout`:素材审核超时; `route_misconfigured`:服务配置错误; `route_temporarily_unavailable`:暂不可用; `generation_expired`:生成已超时; `generation_failed`:生成失败; `input_fetch_failed`:输入素材下载失败; `input_url_not_allowed`:输入素材链接不允许下载; `input_too_large`:输入素材超过大小上限; `input_unsupported`:输入素材格式、尺寸或时长不受支持; `input_rejected`:输入素材未通过内容审核; `input_preparation_failed`:未能及时准备输入素材; `input_duration_exceeded`:输入素材总时长超过型号上限; | | `error.message` | string | 必填 | 供人阅读的错误说明。 长度:1–1024。 | | `error.details` | object | 可选 | 可选的结构化错误详情。 | | `error.details.content_index` | integer | 可选 | 失败输入在原始 content 数组中的下标,从 0 起,文字项也计入。 范围:0–50。 | ### 示例 ```json { "id": "task_32ae2a5177b310d3bc1ec83a98f63889", "object": "video.task", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "model": "dreamina-seedance-2-0-260128", "status": "succeeded", "phase": null, "delivery_status": "available", "settlement_status": "settled", "created_at": 1793167380, "started_at": 1793167381, "completed_at": 1793167382, "usage": { "meter_type": "seedance_video_generation", "video_tokens": 51652, "output_duration_seconds": 5, "input_video_duration_seconds": 0, "resolution": "720p", "fps": 24, "has_reference_video": false }, "billing": { "currency": "USD", "final_actual_cost": "0.361564", "collected_liability_amount": "0.361564", "funding_debit_amount": "0.361564", "refunded_liability_amount": "0.000000", "funding_refund_amount": "0.000000", "outstanding_receivable": "0.000000", "billing_url": "/v1/video/tasks/task_32ae2a5177b310d3bc1ec83a98f63889/billing" }, "output": { "assets": [ { "asset_id": "asset_4d00c4c64086b1e29724c1f2349f3708", "media_type": "video", "role": "result", "url": "https://media.example.invalid/download/result-0001?signature=example", "materialized_at": 1793167385, "expires_at": 1793253785, "delivery_source": "platform", "materialization_status": "completed", "download_status": "available" } ] }, "cancellation": null, "error": null } ``` 用「[查询视频任务](./api-docs-zh-api-video-tasks-retrieve.md)」读取这个对象。 --- ## 创建视频任务 `POST /v1/video/generations` 提交提示词和可选的图片、视频或音频,创建一个异步生成任务,立即返回任务 ID 和预留金额。HTTP 202 表示任务已受理,随后按任务 ID 查询进度和结果。图片和音频也可以用 base64 内嵌,创建请求体最大 64 MiB;大于 2 MiB 的请求体读取期限为 120 秒,每个团队同时最多处理 2 个,超出返回 429 rate_limited。只有一张图片时可以省略 role,按 first_frame 处理;多张图片省略 role 或角色冲突会被拒绝。 - 在请求头 `Authorization: Bearer` 后面放 API 密钥。 - 必须带 `Idempotency-Key`:结果不明时用同一个键原样重试,只会创建一个任务、扣一次费。 - 生成在后台进行:用「[查询视频任务](./api-docs-zh-api-video-tasks-retrieve.md)」轮询,或用 `callback_url` 接收 `task.completed`。 - 创建时按上限预留金额,完成后按实际用量结算;失败或取消的任务不收费。 ### 请求头 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `Authorization` | string | 必填 | Bearer 加一个空格和 API 密钥。缺失或无效时返回 401 authentication_failed。 | | `Content-Type` | string | 必填 | 请求体是 JSON:application/json。 | | `X-Request-ID` | string | 可选 | 你自己的请求标识。无效或重复的值会被替换,最终值在响应头 X-Request-ID 返回。 | | `Idempotency-Key` | string | 必填 | 每个新请求生成一个新值并保存,以字母或数字开头。结果不明时用同一个键和原请求体重试,会返回原响应;同一个键配不同的请求体返回 409 idempotency_conflict。键按团队区分,不按 API 密钥区分。 长度:8–128。 字符:A–Z a–z 0–9 . _ : -。 | ### 请求体 · application/json · 必填 请求体只接受下列字段,其他字段会被拒绝。 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `model` | string | 必填 | 本次使用的模型标识。 `dreamina-seedance-2-0-260128`:Seedance 2.0; `dreamina-seedance-2-0-fast-260128`:Seedance 2.0 Fast; `dreamina-seedance-2-0-mini-260615`:Seedance 2.0 Mini; | | `content` | array | 必填 | 平台限制:最多 9 张图片、3 段视频、3 段音频,共 15 份素材、16 项 content。视频和音频单段均为 2–15 秒,各自合计最多 15 秒。HTTPS 素材由平台下载、扫描、保存并审核后用于生成;准备失败全额释放预留费用,不收生成费。已知素材准备失败时同时返回 content_index。 数量:1–16。 每一项必须且只能符合下列一种结构。 | | `content[] (文本) type` | string | 必填 | 输入项类型。 固定为:text。 | | `content[] (文本) text` | string | 必填 | 生成提示文本。 长度:1–10000。 | | `content[] (图片) type` | string | 必填 | 输入项类型。 固定为:image_url。 | | `content[] (图片) role` | string | 可选 | 素材在输入或结果中的用途。 `reference_image`:参考图片; `first_frame`:首帧; `last_frame`:尾帧; | | `content[] (图片) image_url` | object | 必填 | HTTPS 图片链接、asset:// 平台素材引用,或 JPEG/PNG/WebP 的 base64 data URI。HTTPS 与 base64 可以组合,但不能与 asset:// 引用混用。 必须且只能符合下列一种结构。 | | `content[] (图片) image_url.url (HTTPS 链接)` | string | 必填 | 公网 HTTPS 链接,不含用户信息、字面 IP、本地主机或平台域名;端口省略或为 443,最多 2048 个 UTF-8 字节。 长度:1–2048。 格式:https://。 | | `content[] (图片) image_url.url (素材引用)` | string | 必填 | 使用 asset:// 加本团队已活跃素材的标识,不直接填写来源 HTTPS 地址。 长度:12–128。 格式:asset://ast-…。 | | `content[] (图片) image_url.url (base64 图片)` | string | 必填 | JPEG、PNG 或 WebP 的 base64 data URI。解码后须小于 30 MB,并通过图片头部检查。 长度:≤ 40000023。 格式:data:image/…;base64,…。 | | `content[] (视频) type` | string | 必填 | 输入项类型。 固定为:video_url。 | | `content[] (视频) role` | string | 必填 | 素材在输入或结果中的用途。 固定为:reference_video。 | | `content[] (视频) video_url` | object | 必填 | HTTPS 参考视频链接或 asset:// 平台视频引用;一次请求只能使用同一种来源。 必须且只能符合下列一种结构。 | | `content[] (视频) video_url.url (HTTPS 链接)` | string | 必填 | 公网 HTTPS 链接,不含用户信息、字面 IP、本地主机或平台域名;端口省略或为 443,最多 2048 个 UTF-8 字节。 长度:1–2048。 格式:https://。 | | `content[] (视频) video_url.url (素材引用)` | string | 必填 | 使用 asset:// 加本团队已活跃素材的标识,不直接填写来源 HTTPS 地址。 长度:12–128。 格式:asset://ast-…。 | | `content[] (音频) type` | string | 必填 | 输入项类型。 固定为:audio_url。 | | `content[] (音频) role` | string | 必填 | 素材在输入或结果中的用途。 固定为:reference_audio。 | | `content[] (音频) audio_url` | object | 必填 | HTTPS 参考音频链接、asset:// 平台音频引用,或 WAV/MP3 的 base64 data URI(含 wav、x-wav、mpeg、mp3 别名)。HTTPS 与 base64 可以组合,但不能与 asset:// 引用混用。 必须且只能符合下列一种结构。 | | `content[] (音频) audio_url.url (HTTPS 链接)` | string | 必填 | 公网 HTTPS 链接,不含用户信息、字面 IP、本地主机或平台域名;端口省略或为 443,最多 2048 个 UTF-8 字节。 长度:1–2048。 格式:https://。 | | `content[] (音频) audio_url.url (素材引用)` | string | 必填 | 使用 asset:// 加本团队已活跃素材的标识,不直接填写来源 HTTPS 地址。 长度:12–128。 格式:asset://ast-…。 | | `content[] (音频) audio_url.url (base64 音频)` | string | 必填 | WAV 或 MP3 的 base64 data URI,最大 15 MB、时长 2 至 15 秒;兼容 audio/x-wav 和 audio/mp3。 长度:≤ 20000024。 格式:data:audio/…;base64,…。 | | `resolution` | string | 可选 | 输出分辨率。Seedance 2.0 Fast 和 Mini 只接受 480p 或 720p。 默认:720p。 `480p`:四百八十线分辨率; `720p`:七百二十线分辨率; `1080p`:一千零八十线分辨率; | | `ratio` | string | 可选 | 画面宽高比。 默认:adaptive。 `adaptive`:自适应宽高比; `21:9`:超宽画面; `16:9`:横向宽屏; `4:3`:横向画面; `1:1`:正方形画面; `3:4`:竖向画面; `9:16`:竖向宽屏; | | `duration` | integer | 可选 | 输出时长,4–15 的整数秒,默认 5 秒。 默认:5。 范围:4–15。 | | `generate_audio` | boolean | 可选 | 是否生成音频。 默认:false。 | | `seed` | integer | 可选 | 随机种子,默认 -1,表示随机。 范围:-1–4294967295。 默认:-1。 | | `watermark` | boolean | 可选 | 是否添加水印。 默认:false。 | | `callback_url` | string | 可选 | 任务结束后接收回调的地址。必须与本团队在控制台已验证、未撤销、未过期的回调地址逐字节相同,否则返回 400 callback_target_not_verified。用同一个 Idempotency-Key 重试时不能增加、去掉或更换它,否则返回 409 idempotency_conflict。 长度:1–2048。 格式:https://。 | 暂不支持的参数:`return_last_frame`:设为 `true` 时返回 400 `unsupported_capability`。 `draft`, `output_format`, `omni_reference_task_type`, `execution_expires_after`, `priority`, `safety_identifier`:当前模型不接受,传入即返回 400 `invalid_request`。 ### 返回 · HTTP 202 已受理:任务已创建,金额已预留。响应头 Location 是任务的查询路径。 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `id` | string | 必填 | 平台任务标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `object` | string | 必填 | 对象类型标识。 固定为:video.task。 | | `request_id` | string | 必填 | 请求关联标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `billing_correlation_id` | string | 必填 | 费用关联标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `model` | string | 必填 | 本次使用的模型标识。 `dreamina-seedance-2-0-260128`:Seedance 2.0; `dreamina-seedance-2-0-fast-260128`:Seedance 2.0 Fast; `dreamina-seedance-2-0-mini-260615`:Seedance 2.0 Mini; | | `status` | string | 必填 | 当前状态,具体含义见对应取值。 固定为:queued。 | | `phase` | string | 可选 | 生成开始前正在准备输入素材。 固定为:preparing_assets。 | | `settlement_status` | string | 必填 | 当前费用结算状态。 固定为:reserved。 | | `quote` | object | 必填 | 接受任务时的预留报价。 | | `quote.currency` | string | 必填 | 金额币种,美元。 固定为:USD。 | | `quote.reserved_amount` | string | 必填 | 接受任务所需的预留金额。 格式:6 位小数。 | | `quote.meter_type` | string | 必填 | 用量计量类型。 固定为:seedance_video_generation。 | | `quote.estimated_usage_upper_bound` | object | 必填 | 预估用量上界。 | | `quote.estimated_usage_upper_bound.video_tokens` | integer | 必填 | 视频计量单位数;实际用量允许为空的情况以字段类型为准。 范围:≥ 1。 | | `quote.price_version` | string | 必填 | 本次报价的价格版本。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `quote.quote_policy_version` | string | 必填 | 预留报价策略版本。 长度:1–128。 字符:A–Z a–z 0–9 . _ : -。 | | `created_at` | integer | 必填 | 创建时间,Unix 整数秒。 范围:≥ 0。 | #### 响应头 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `X-Request-ID` | string | 必填 | 本次请求的标识:你提供的有效值,或替换后的值。 | | `Location` | string | 必填 | 已接受对象的规范查询路径。 | ### 状态码 每个 `error.code` 的处理办法见「[错误码](./api-docs-zh-api-errors.md)」。 | 状态码 | 说明 | error.code | | --- | --- | --- | | 202 | 已受理:任务已创建,金额已预留。 响应头:`Location` | | | 400 | 请求格式或参数不对,或引用的素材、回调地址不能用。 | asset_not_found, asset_unusable, callback_target_not_verified, input_video_duration_exceeded, invalid_request, unsupported_capability | | 401 | API 密钥缺失或无效。 响应头:`WWW-Authenticate` | authentication_failed | | 409 | 幂等键冲突或重放已过期,API 密钥未就绪,或素材还在处理。 | asset_not_ready, idempotency_conflict, idempotency_replay_expired, key_provisioning_incomplete | | 429 | 余额不足、请求太频繁,或素材存储已满。 响应头:`Retry-After` | asset_storage_quota_exceeded, insufficient_balance, rate_limited | | 500 | 服务内部错误。 | internal_error | | 503 | 服务暂时不可用,或暂停接收新任务。 | dependency_unavailable, funding_intake_closed | ### 相关指南 - [快速开始](./api-docs-zh-quickstart.md) - [生成视频](./api-docs-zh-video-generate.md) - [幂等与重试](./api-docs-zh-idempotency.md) - [处理错误](./api-docs-zh-errors.md) ### 请求示例 ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} printf '%s\n' "保存 IDEMPOTENCY_KEY,重试时原样复用:${IDEMPOTENCY_KEY}" >&2 curl --request POST "${CHENVOYA_BASE_URL}/v1/video/generations" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" \ --header "Idempotency-Key: ${IDEMPOTENCY_KEY}" \ --header 'Content-Type: application/json' \ --data '{ "model": "dreamina-seedance-2-0-260128", "content": [ { "type": "text", "text": "A paper boat crossing a moonlit lake." } ] }' ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} idempotency = os.environ.get("IDEMPOTENCY_KEY") or str(uuid.uuid4()) print("保存 IDEMPOTENCY_KEY,重试时原样复用:" + idempotency, file=sys.stderr) headers["Idempotency-Key"] = idempotency path = "/v1/video/generations" body_text = "{\"model\":\"dreamina-seedance-2-0-260128\",\"content\":[{\"type\":\"text\",\"text\":\"A paper boat crossing a moonlit lake.\"}]}" headers["Content-Type"] = "application/json" body = json.dumps(json.loads(body_text)).encode("utf-8") request = urllib.request.Request(base + path, data=body, headers=headers, method="POST") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const idempotency = process.env.IDEMPOTENCY_KEY || crypto.randomUUID(); console.error("保存 IDEMPOTENCY_KEY,重试时原样复用:" + idempotency); headers["Idempotency-Key"] = idempotency; const path = "/v1/video/generations"; let bodyText = "{\"model\":\"dreamina-seedance-2-0-260128\",\"content\":[{\"type\":\"text\",\"text\":\"A paper boat crossing a moonlit lake.\"}]}"; headers["Content-Type"] = "application/json"; const body = JSON.stringify(JSON.parse(bodyText)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "POST", headers, body: body}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "crypto/rand" "fmt" "io" "net/http" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } path := "/v1/video/generations" bodyText := "{\"model\":\"dreamina-seedance-2-0-260128\",\"content\":[{\"type\":\"text\",\"text\":\"A paper boat crossing a moonlit lake.\"}]}" request, err := http.NewRequest("POST", base+path, strings.NewReader(bodyText)) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) request.Header.Set("Content-Type", "application/json") idempotency := os.Getenv("IDEMPOTENCY_KEY") if idempotency == "" { idempotency = newOperationKey() } fmt.Fprintln(os.Stderr, "保存 IDEMPOTENCY_KEY,重试时原样复用:"+idempotency) request.Header.Set("Idempotency-Key", idempotency) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } // newOperationKey returns a random UUID; save it to retry with the same value. func newOperationKey() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { fail(err.Error()) } b[6] = b[6]&0x0f | 0x40 b[8] = b[8]&0x3f | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } ``` ### 响应示例 202 ```json { "id": "task_32ae2a5177b310d3bc1ec83a98f63889", "object": "video.task", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "model": "dreamina-seedance-2-0-260128", "status": "queued", "settlement_status": "reserved", "quote": { "currency": "USD", "reserved_amount": "0.782453", "meter_type": "seedance_video_generation", "estimated_usage_upper_bound": { "video_tokens": 111779 }, "price_version": "price_06c9ad4cf0af577624218747c25fdb37", "quote_policy_version": "seedance2_quote_v1" }, "created_at": 1793167380 } ``` 400 ```json { "error": { "code": "invalid_request", "message": "request parameters are invalid" }, "trace_id": "trace_error_example" } ``` 401 ```json { "error": { "code": "authentication_failed", "message": "authentication failed" }, "trace_id": "trace_error_example" } ``` 409 ```json { "error": { "code": "idempotency_conflict", "message": "the idempotency key was used with a different canonical request" }, "trace_id": "trace_error_example" } ``` 429 ```json { "error": { "code": "insufficient_balance", "message": "available USD balance is below the required reservation", "details": { "currency": "USD", "reserved_amount": "0.782453", "meter_type": "seedance_video_generation", "estimated_usage_upper_bound": { "video_tokens": 111779 }, "price_version": "price_06c9ad4cf0af577624218747c25fdb37" } }, "trace_id": "trace_error_example" } ``` 500 ```json { "error": { "code": "internal_error", "message": "internal server error" }, "trace_id": "trace_error_example" } ``` 503 ```json { "error": { "code": "dependency_unavailable", "message": "service is not ready" }, "trace_id": "trace_error_example" } ``` --- ## 查询视频任务 `GET /v1/video/tasks/{task_id}` 读取任务当前状态、交付情况、用量与结算摘要。任务状态、交付状态和结算状态分别表示执行、文件交付与费用处理进度。结果下载过期或暂不可用时,下载地址为空。 - 在请求头 `Authorization: Bearer` 后面放 API 密钥。 - 返回任务状态、交付、用量和结算摘要;`status` 为终态前继续查询。 - 成功后从 `output.assets[].url` 下载结果;链接过期或暂不可用时为空。 ### 请求头 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `Authorization` | string | 必填 | Bearer 加一个空格和 API 密钥。缺失或无效时返回 401 authentication_failed。 | | `X-Request-ID` | string | 可选 | 你自己的请求标识。无效或重复的值会被替换,最终值在响应头 X-Request-ID 返回。 | ### 路径参数 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `task_id` | string | 必填 | 平台视频任务标识 | ### 返回 · HTTP 200 当前任务、交付、用量与结算信息。 返回「[视频任务对象](./api-docs-zh-api-video-tasks-object.md)」,字段说明见该页。 #### 响应头 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `X-Request-ID` | string | 必填 | 本次请求的标识:你提供的有效值,或替换后的值。 | ### 状态码 每个 `error.code` 的处理办法见「[错误码](./api-docs-zh-api-errors.md)」。 | 状态码 | 说明 | error.code | | --- | --- | --- | | 200 | 当前任务、交付、用量与结算信息。 | | | 400 | 请求无效。 | invalid_request | | 401 | API 密钥缺失或无效。 响应头:`WWW-Authenticate` | authentication_failed | | 404 | 任务不存在或对本团队不可见。 | task_not_found | | 429 | 请求频率限制拒绝此非创建操作。 响应头:`Retry-After` | rate_limited | | 500 | 服务内部错误。 | internal_error | | 503 | 服务暂时不可用。 | dependency_unavailable | ### 相关指南 - [任务状态与结果](./api-docs-zh-video-tasks.md) - [快速开始](./api-docs-zh-quickstart.md) - [接收回调](./api-docs-zh-callbacks.md) ### 请求示例 ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/video/tasks/${TASK_ID}" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} TASK_ID = os.environ["TASK_ID"] path = "/v1/video/tasks/{task_id}".replace("{task_id}", urllib.parse.quote(TASK_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const TASK_ID = process.env.TASK_ID; if (!TASK_ID) throw new Error("Set TASK_ID"); const path = "/v1/video/tasks/{task_id}".replace("{task_id}", encodeURIComponent(TASK_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } taskID := os.Getenv("TASK_ID") if taskID == "" { fail("Set TASK_ID") } path := strings.Replace("/v1/video/tasks/{task_id}", "{task_id}", url.PathEscape(taskID), 1) request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` ### 响应示例 200 ```json { "id": "task_32ae2a5177b310d3bc1ec83a98f63889", "object": "video.task", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "model": "dreamina-seedance-2-0-260128", "status": "succeeded", "phase": null, "delivery_status": "available", "settlement_status": "settled", "created_at": 1793167380, "started_at": 1793167381, "completed_at": 1793167382, "usage": { "meter_type": "seedance_video_generation", "video_tokens": 51652, "output_duration_seconds": 5, "input_video_duration_seconds": 0, "resolution": "720p", "fps": 24, "has_reference_video": false }, "billing": { "currency": "USD", "final_actual_cost": "0.361564", "collected_liability_amount": "0.361564", "funding_debit_amount": "0.361564", "refunded_liability_amount": "0.000000", "funding_refund_amount": "0.000000", "outstanding_receivable": "0.000000", "billing_url": "/v1/video/tasks/task_32ae2a5177b310d3bc1ec83a98f63889/billing" }, "output": { "assets": [ { "asset_id": "asset_4d00c4c64086b1e29724c1f2349f3708", "media_type": "video", "role": "result", "url": "https://media.example.invalid/download/result-0001?signature=example", "materialized_at": 1793167385, "expires_at": 1793253785, "delivery_source": "platform", "materialization_status": "completed", "download_status": "available" } ] }, "cancellation": null, "error": null } ``` 400 ```json { "error": { "code": "invalid_request", "message": "request parameters are invalid" }, "trace_id": "trace_error_example" } ``` 401 ```json { "error": { "code": "authentication_failed", "message": "authentication failed" }, "trace_id": "trace_error_example" } ``` 404 ```json { "error": { "code": "task_not_found", "message": "task was not found" }, "trace_id": "trace_error_example" } ``` 429 ```json { "error": { "code": "rate_limited", "message": "request rate limit exceeded" }, "trace_id": "trace_error_example" } ``` 500 ```json { "error": { "code": "internal_error", "message": "internal server error" }, "trace_id": "trace_error_example" } ``` 503 ```json { "error": { "code": "dependency_unavailable", "message": "service is not ready" }, "trace_id": "trace_error_example" } ``` --- ## 列出团队任务 `GET /v1/video/tasks` 列出当前团队的全部任务,不限于创建任务的那一把 API Key,按创建时间从新到旧排序,同一时刻用任务标识排序。空团队返回空列表。列表含标识、模型、状态、时间,以及存在时的取消投影;用量、费用、结果及错误需查询任务详情。 - 在请求头 `Authorization: Bearer` 后面放 API 密钥。 - 列出团队的全部任务,不限于创建任务的那一把 API 密钥,从新到旧排列。 - 用 `next_cursor` 翻页;最后一页为空。 - 用量、费用、结果和错误请按任务 ID 查询任务详情。 ### 请求头 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `Authorization` | string | 必填 | Bearer 加一个空格和 API 密钥。缺失或无效时返回 401 authentication_failed。 | | `X-Request-ID` | string | 可选 | 你自己的请求标识。无效或重复的值会被替换,最终值在响应头 X-Request-ID 返回。 | ### 查询参数 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `cursor` | string | 可选 | 上一页 next_cursor 返回的不透明游标。不是服务为本团队此列表签发的游标返回 invalid_request。 长度:1–256。 | | `page_size` | integer | 可选 | 此页最多返回的任务数。 默认:50。 范围:1–100。 | ### 返回 · HTTP 200 本团队的一页任务。 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `object` | string | 必填 | 对象类型标识。 固定为:list。 | | `data` | array | 必填 | 本页的任务列表项。 数量:≤ 100。 | | `data.id` | string | 必填 | 平台任务标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `data.request_id` | string | 必填 | 请求关联标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `data.billing_correlation_id` | string | 必填 | 费用关联标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `data.model` | string | 必填 | 本次使用的模型标识。 `dreamina-seedance-2-0-260128`:Seedance 2.0; `dreamina-seedance-2-0-fast-260128`:Seedance 2.0 Fast; `dreamina-seedance-2-0-mini-260615`:Seedance 2.0 Mini; | | `data.status` | string | 必填 | 当前状态,具体含义见对应取值。 `queued`:排队中; `processing`:处理中; `succeeded`:已成功; `failed`:已失败; `cancelled`:已取消; | | `data.phase` | string | 必填,可能为空 | 任务执行阶段;没有当前阶段时为空。 `preparing_assets`:准备素材; `waiting_route`:等待处理; `submitting`:准备生成; `provider_processing`:生成中; `reconciling`:确认结果中; `finalizing`:整理结果; `settlement_pending`:等待结算; `null`:空值; | | `data.delivery_status` | string | 必填 | 结果交付状态。 `pending`:待交付; `materializing`:准备下载; `available`:可下载; `output_unavailable`:结果不可用; | | `data.settlement_status` | string | 必填 | 当前费用结算状态。 `reserved`:已预留费用; `pending`:待处理; `reconciliation_required`:需要核对; `settled`:已结算; `settled_with_receivable`:已结算,仍有应收金额; | | `data.created_at` | integer | 必填 | 创建时间,Unix 整数秒。 范围:≥ 0。 | | `data.started_at` | integer | 必填,可能为空 | 任务开始时间,未开始时为空。 范围:≥ 0。 | | `data.completed_at` | integer | 必填,可能为空 | 任务完成时间,未完成时为空。 范围:≥ 0。 | | `data.cancellation` | object | 可选,可能为空 | 存在取消操作时附带其稳定标识、状态及原因码;没有操作时省略。 | | `data.cancellation.cancel_operation_id` | string | 必填 | 取消操作的稳定标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `data.cancellation.status` | string | 必填 | 当前状态,具体含义见对应取值。 `requested`:已请求取消; `effective`:取消已生效; `not_effective`:取消未生效; `failed`:失败; | | `data.cancellation.reason_code` | string | 必填,可能为空 | 取消未生效或失败时的原因码;已请求或已生效时为空。 长度:1–96。 字符:a–z 0–9 _。 | | `next_cursor` | string | 必填,可能为空 | 下一页游标;最后一页为空。 长度:1–256。 | #### 响应头 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `X-Request-ID` | string | 必填 | 本次请求的标识:你提供的有效值,或替换后的值。 | ### 状态码 每个 `error.code` 的处理办法见「[错误码](./api-docs-zh-api-errors.md)」。 | 状态码 | 说明 | error.code | | --- | --- | --- | | 200 | 本团队的一页任务。 | | | 400 | 请求无效。 | invalid_request | | 401 | API 密钥缺失或无效。 响应头:`WWW-Authenticate` | authentication_failed | | 429 | 请求频率限制拒绝此非创建操作。 响应头:`Retry-After` | rate_limited | | 500 | 服务内部错误。 | internal_error | | 503 | 服务暂时不可用。 | dependency_unavailable | ### 相关指南 - [任务状态与结果](./api-docs-zh-video-tasks.md) - [请求约定](./api-docs-zh-conventions.md) ### 请求示例 ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/video/tasks" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} path = "/v1/video/tasks" request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const path = "/v1/video/tasks"; const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } path := "/v1/video/tasks" request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` ### 响应示例 200 ```json { "object": "list", "data": [ { "id": "task_32ae2a5177b310d3bc1ec83a98f63889", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "model": "dreamina-seedance-2-0-260128", "status": "succeeded", "phase": null, "delivery_status": "available", "settlement_status": "settled", "created_at": 1793167380, "started_at": 1793167381, "completed_at": 1793167382 } ], "next_cursor": null } ``` 400 ```json { "error": { "code": "invalid_request", "message": "request parameters are invalid" }, "trace_id": "trace_error_example" } ``` 401 ```json { "error": { "code": "authentication_failed", "message": "authentication failed" }, "trace_id": "trace_error_example" } ``` 429 ```json { "error": { "code": "rate_limited", "message": "request rate limit exceeded" }, "trace_id": "trace_error_example" } ``` 500 ```json { "error": { "code": "internal_error", "message": "internal server error" }, "trace_id": "trace_error_example" } ``` 503 ```json { "error": { "code": "dependency_unavailable", "message": "service is not ready" }, "trace_id": "trace_error_example" } ``` --- ## 取消视频任务 `POST /v1/video/tasks/{task_id}/cancel` 需要 service.use 权限,必须提供 Idempotency-Key,请求体必须为空。生成开始前取消立即生效,任务变为 cancelled,全额释放预留;资金处理挂起时仍返回 effective,随后完成释放。生成开始后返回 not_effective,原任务继续生成并照常结算。相同操作键重放原响应;同键换任务返回 idempotency_conflict。新键引用同一取消操作并冻结当时任务状态;没有取消操作的终态任务返回 task_not_cancellable。未知或其他团队任务与查询接口一样返回 task_not_found,拒绝请求不保留操作键。 - 在请求头 `Authorization: Bearer` 后面放 API 密钥;请求体为空。 - 必须带 `Idempotency-Key`;同一个键重放原响应。 - 生成开始前取消立即生效,任务变为 `cancelled`,预留全额释放。 - 生成开始后返回 `not_effective`,任务继续生成并照常结算。 ### 请求头 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `Authorization` | string | 必填 | Bearer 加一个空格和 API 密钥。缺失或无效时返回 401 authentication_failed。 | | `X-Request-ID` | string | 可选 | 你自己的请求标识。无效或重复的值会被替换,最终值在响应头 X-Request-ID 返回。 | | `Idempotency-Key` | string | 必填 | 每个新请求生成一个新值并保存,以字母或数字开头。结果不明时用同一个键和原请求体重试,会返回原响应;同一个键配不同的请求体返回 409 idempotency_conflict。键按团队区分,不按 API 密钥区分。 长度:8–128。 字符:A–Z a–z 0–9 . _ : -。 | ### 路径参数 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `task_id` | string | 必填 | 平台视频任务标识 | ### 返回 · HTTP 202 不可变的取消操作快照;effective 表示已取消,not_effective 表示本次取消未生效,须继续查询任务与账单。 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `task_id` | string | 必填 | 本团队的视频任务标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `cancel_operation_id` | string | 必填 | 取消操作的稳定标识,同一任务的新操作键引用同一个标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `cancel_status` | string | 必填 | 取消操作状态;首发直接回答已生效或未生效。 `requested`:已请求取消; `effective`:取消已生效; `not_effective`:取消未生效; `failed`:失败; | | `task_status_at_response` | string | 必填 | 该操作键首次应答时冻结的任务状态,重放时不会更新。 `queued`:排队中; `processing`:处理中; `succeeded`:已成功; `failed`:已失败; `cancelled`:已取消; | | `reason_code` | string | 必填,可能为空 | 已生效时为空;生成已开始而未生效时为 generation_already_started。 长度:1–96。 字符:a–z 0–9 _。 | #### 响应头 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `X-Request-ID` | string | 必填 | 本次请求的标识:你提供的有效值,或替换后的值。 | | `Location` | string | 必填 | 已接受对象的规范查询路径。 | ### 状态码 每个 `error.code` 的处理办法见「[错误码](./api-docs-zh-api-errors.md)」。 | 状态码 | 说明 | error.code | | --- | --- | --- | | 202 | 不可变的取消操作快照;effective 表示已取消,not_effective 表示本次取消未生效,须继续查询任务与账单。 响应头:`Location` | | | 400 | 请求无效。 | invalid_request | | 401 | API 密钥缺失或无效。 响应头:`WWW-Authenticate` | authentication_failed | | 404 | 任务不存在或对本团队不可见。 | task_not_found | | 409 | 已有操作键对应不同请求,或终态任务没有可引用的取消操作。 | idempotency_conflict, idempotency_replay_expired, task_not_cancellable | | 429 | 请求频率限制拒绝此非创建操作。 响应头:`Retry-After` | rate_limited | | 500 | 服务内部错误。 | internal_error | | 503 | 服务暂时不可用。 | dependency_unavailable | ### 相关指南 - [任务状态与结果](./api-docs-zh-video-tasks.md) - [幂等与重试](./api-docs-zh-idempotency.md) - [计费方式](./api-docs-zh-billing.md) ### 请求示例 ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} printf '%s\n' "保存 IDEMPOTENCY_KEY,重试时原样复用:${IDEMPOTENCY_KEY}" >&2 curl --request POST "${CHENVOYA_BASE_URL}/v1/video/tasks/${TASK_ID}/cancel" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" \ --header "Idempotency-Key: ${IDEMPOTENCY_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} idempotency = os.environ.get("IDEMPOTENCY_KEY") or str(uuid.uuid4()) print("保存 IDEMPOTENCY_KEY,重试时原样复用:" + idempotency, file=sys.stderr) headers["Idempotency-Key"] = idempotency TASK_ID = os.environ["TASK_ID"] path = "/v1/video/tasks/{task_id}/cancel".replace("{task_id}", urllib.parse.quote(TASK_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="POST") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const idempotency = process.env.IDEMPOTENCY_KEY || crypto.randomUUID(); console.error("保存 IDEMPOTENCY_KEY,重试时原样复用:" + idempotency); headers["Idempotency-Key"] = idempotency; const TASK_ID = process.env.TASK_ID; if (!TASK_ID) throw new Error("Set TASK_ID"); const path = "/v1/video/tasks/{task_id}/cancel".replace("{task_id}", encodeURIComponent(TASK_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "POST", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "crypto/rand" "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } taskID := os.Getenv("TASK_ID") if taskID == "" { fail("Set TASK_ID") } path := strings.Replace("/v1/video/tasks/{task_id}/cancel", "{task_id}", url.PathEscape(taskID), 1) request, err := http.NewRequest("POST", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) idempotency := os.Getenv("IDEMPOTENCY_KEY") if idempotency == "" { idempotency = newOperationKey() } fmt.Fprintln(os.Stderr, "保存 IDEMPOTENCY_KEY,重试时原样复用:"+idempotency) request.Header.Set("Idempotency-Key", idempotency) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } // newOperationKey returns a random UUID; save it to retry with the same value. func newOperationKey() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { fail(err.Error()) } b[6] = b[6]&0x0f | 0x40 b[8] = b[8]&0x3f | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } ``` ### 响应示例 202 ```json { "task_id": "task_32ae2a5177b310d3bc1ec83a98f63889", "cancel_operation_id": "cancelop_ab99971fbec36056096527b7494b2059", "cancel_status": "requested", "task_status_at_response": "processing", "reason_code": null } ``` 400 ```json { "error": { "code": "invalid_request", "message": "request parameters are invalid" }, "trace_id": "trace_error_example" } ``` 401 ```json { "error": { "code": "authentication_failed", "message": "authentication failed" }, "trace_id": "trace_error_example" } ``` 404 ```json { "error": { "code": "task_not_found", "message": "task was not found" }, "trace_id": "trace_error_example" } ``` 409 ```json { "error": { "code": "task_not_cancellable", "message": "the terminal task has no cancellation operation" }, "trace_id": "trace_error_example" } ``` 429 ```json { "error": { "code": "rate_limited", "message": "request rate limit exceeded" }, "trace_id": "trace_error_example" } ``` 500 ```json { "error": { "code": "internal_error", "message": "internal server error" }, "trace_id": "trace_error_example" } ``` 503 ```json { "error": { "code": "dependency_unavailable", "message": "service is not ready" }, "trace_id": "trace_error_example" } ``` --- ## 查询单任务账单 `GET /v1/video/tasks/{task_id}/billing` 读取本团队任务的稳定账单汇总及一页有序、不可变的账单事件。用 cursor 和 page_size 翻页;汇总属于整个任务,不是当前事件页的小计。任务不存在或对本团队不可见时返回 task_not_found。 - 在请求头 `Authorization: Bearer` 后面放 API 密钥。 - 汇总属于整个任务,不是当前事件页的小计。 - 用 `cursor` 和 `page_size` 翻页读取不可变的账单事件。 - 金额是六位小数的美元字符串。 ### 请求头 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `Authorization` | string | 必填 | Bearer 加一个空格和 API 密钥。缺失或无效时返回 401 authentication_failed。 | | `X-Request-ID` | string | 可选 | 你自己的请求标识。无效或重复的值会被替换,最终值在响应头 X-Request-ID 返回。 | ### 路径参数 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `task_id` | string | 必填 | 平台视频任务标识 | ### 查询参数 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `cursor` | string | 可选 | 下一页不可变账单事件的不透明游标。 长度:1–256。 | | `page_size` | integer | 可选 | 本页最多返回的账单事件数。 默认:50。 范围:1–100。 | ### 返回 · HTTP 200 稳定的任务级账单汇总及一页有序、不可变的事件。 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `object` | string | 必填 | 单任务账单对象类型标识。 固定为:video.task.billing。 | | `task_id` | string | 必填 | 本团队任务标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `request_id` | string | 必填 | 请求标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `billing_correlation_id` | string | 必填 | 账单关联标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `model` | string | 必填 | 本任务使用的模型标识。 `dreamina-seedance-2-0-260128`:Seedance 2.0; `dreamina-seedance-2-0-fast-260128`:Seedance 2.0 Fast; `dreamina-seedance-2-0-mini-260615`:Seedance 2.0 Mini; | | `task_status` | string | 必填 | 任务执行状态。 `queued`:排队中; `processing`:处理中; `succeeded`:已成功; `failed`:已失败; `cancelled`:已取消; | | `settlement_status` | string | 必填 | 任务费用结算状态。 `reserved`:已预留费用; `pending`:待处理; `reconciliation_required`:需要核对; `settled`:已结算; `settled_with_receivable`:已结算,仍有应收金额; | | `currency` | string | 必填 | 金额币种,美元。 固定为:USD。 | | `reserved_amount` | string | 必填 | 任务预留金额。 格式:6 位小数。 | | `charged_amount` | string | 必填 | 任务计费金额。 格式:6 位小数。 | | `collected_liability_amount` | string | 必填 | 已收取的计费金额。 格式:6 位小数。 | | `funding_debit_amount` | string | 必填 | 实际扣减的资金金额。 格式:6 位小数。 | | `refunded_liability_amount` | string | 必填 | 已退回的计费金额。 格式:6 位小数。 | | `funding_refund_amount` | string | 必填 | 实际退回的资金金额。 格式:6 位小数。 | | `outstanding_receivable` | string | 必填 | 尚未收取的应收金额。失败或已取消且已结算的任务为零。 格式:6 位小数。 | | `final_actual_cost` | string | 必填,可能为空 | 最终实际费用。任务排队、处理中,或结算处于 reserved、pending、reconciliation_required 时为空;settled 或 settled_with_receivable 时为金额。失败或已取消且已结算的任务为零。 格式:6 位小数。 | | `events` | array | 必填 | 当前页的不可变账单事件列表。 数量:≤ 100。 | | `events.billing_event_id` | string | 必填 | 账单事件标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `events.task_id` | string | 必填 | 事件所属任务标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `events.request_id` | string | 必填 | 事件关联的请求标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `events.billing_correlation_id` | string | 必填 | 账单关联标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `events.event_type` | string | 必填 | 账单事件类型。 `hold_created`:创建费用预留; `hold_increased`:增加费用预留; `hold_reduced`:减少费用预留; `charge_captured`:收取费用; `supplemental_charge`:补充计费; `receivable_created`:新增应收; `receivable_payment`:支付应收; `receivable_reduced`:减少应收; `hold_released`:释放费用预留; `refund_posted`:退款已记账; `charge_waived`:费用已免除; `manual_adjustment`:人工调整; | | `events.billing_component` | string | 必填 | 计费组成项。 `generation_output`:生成结果; `reference_video`:参考视频; `reference_image`:参考图片; `reference_audio`:参考音频; `web_search`:联网搜索; `other`:其他计费组成项; | | `events.amount` | string | 必填 | 本事件金额。charge_waived 时为零;hold_reduced 时大于零。 格式:6 位小数。 | | `events.charge_amount` | string | 必填 | 本事件的计费金额。 格式:6 位小数。 | | `events.refund_amount` | string | 必填 | 本事件的退款金额。 格式:6 位小数。 | | `events.collected_liability_amount` | string | 必填 | 本事件已收取的计费金额。 格式:6 位小数。 | | `events.funding_debit_amount` | string | 必填 | 本事件实际扣减的资金金额。 格式:6 位小数。 | | `events.funding_refund_amount` | string | 必填 | 本事件实际退回的资金金额。 格式:6 位小数。 | | `events.receivable_amount` | string | 必填 | 本事件的应收变动金额。 格式:6 位小数。 | | `events.receivable_direction` | string | 必填,可能为空 | 应收变动方向。receivable_created 为 increase;receivable_payment 或 receivable_reduced 为 decrease;其他事件为空。 `increase`:增加应收; `decrease`:减少应收; `null`:空值; | | `events.adjustment_direction` | string | 必填,可能为空 | 人工调整方向,manual_adjustment 时为 debit 或 credit,其他事件为空。 `debit`:扣减方向; `credit`:增加方向; `null`:空值; | | `events.audit_reason` | string | 必填,可能为空 | 人工调整的审计原因;manual_adjustment 时非空,其他事件为空。 长度:1–1024。 | | `events.component_detail_code` | string | 必填,可能为空 | billing_component 为 other 时的非空明细码;其余组成项为空。 长度:1–96。 字符:a–z 0–9 _。 | | `events.currency` | string | 必填 | 金额币种,美元。 固定为:USD。 | | `events.settlement_status` | string | 必填 | 事件记录的费用结算状态。 `reserved`:已预留费用; `pending`:待处理; `reconciliation_required`:需要核对; `settled`:已结算; `settled_with_receivable`:已结算,仍有应收金额; | | `events.created_at` | integer | 必填 | 事件创建时间,Unix 整数秒。 范围:≥ 0。 | | `events.billing_operation_id` | string | 可选 | 费用操作标识,hold_reduced 事件必须提供。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `events.quote_revision_before` | integer | 可选 | 费用操作前的报价修订号,hold_reduced 事件必须提供。 范围:≥ 0。 | | `events.quote_revision_after` | integer | 可选 | 费用操作后的报价修订号,hold_reduced 事件必须提供。 范围:≥ 1。 | | `events.reservation_revision_before` | integer | 可选 | 费用操作前的预留修订号,hold_reduced 事件必须提供。 范围:≥ 0。 | | `events.reservation_revision_after` | integer | 可选 | 费用操作后的预留修订号,hold_reduced 事件必须提供。 范围:≥ 1。 | | `next_cursor` | string | 必填,可能为空 | 下一页游标,可为空。 长度:1–256。 | | `reconciliation` | object | 必填,可能为空 | 结算状态为 reconciliation_required 时返回核对摘要;reserved、pending、settled 和 settled_with_receivable 时为空。 | | `reconciliation.case_id` | string | 必填 | 核对事项标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `reconciliation.reason_code` | string | 必填 | 费用需要核对的原因码。 `missing_billable_usage`:缺少可计费用量; `usage_schema_drift`:用量结构发生变化; `unknown_usage_unit`:用量单位未知; `price_rule_not_unique`:价格规则不唯一; `output_unavailable`:结果不可用; | | `reconciliation.missing_meters` | array | 必填 | 缺少的计量字段列表;原因为 missing_billable_usage 时至少有一项,列表内不重复。 数量:≤ 16。 | | `reconciliation.customer_action_required` | boolean | 必填 | 是否需要客户操作。 | #### 响应头 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `X-Request-ID` | string | 必填 | 本次请求的标识:你提供的有效值,或替换后的值。 | ### 状态码 每个 `error.code` 的处理办法见「[错误码](./api-docs-zh-api-errors.md)」。 | 状态码 | 说明 | error.code | | --- | --- | --- | | 200 | 稳定的任务级账单汇总及一页有序、不可变的事件。 | | | 400 | 请求无效。 | invalid_request | | 401 | API 密钥缺失或无效。 响应头:`WWW-Authenticate` | authentication_failed | | 404 | 任务不存在或对本团队不可见。 | task_not_found | | 429 | 请求频率限制拒绝此非创建操作。 响应头:`Retry-After` | rate_limited | | 500 | 服务内部错误。 | internal_error | | 503 | 服务暂时不可用。 | dependency_unavailable | ### 相关指南 - [对账](./api-docs-zh-billing-reconcile.md) - [计费方式](./api-docs-zh-billing.md) ### 请求示例 ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/video/tasks/${TASK_ID}/billing" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} TASK_ID = os.environ["TASK_ID"] path = "/v1/video/tasks/{task_id}/billing".replace("{task_id}", urllib.parse.quote(TASK_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const TASK_ID = process.env.TASK_ID; if (!TASK_ID) throw new Error("Set TASK_ID"); const path = "/v1/video/tasks/{task_id}/billing".replace("{task_id}", encodeURIComponent(TASK_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } taskID := os.Getenv("TASK_ID") if taskID == "" { fail("Set TASK_ID") } path := strings.Replace("/v1/video/tasks/{task_id}/billing", "{task_id}", url.PathEscape(taskID), 1) request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` ### 响应示例 200 ```json { "object": "video.task.billing", "task_id": "task_32ae2a5177b310d3bc1ec83a98f63889", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "model": "dreamina-seedance-2-0-260128", "task_status": "succeeded", "settlement_status": "settled", "currency": "USD", "reserved_amount": "0.782453", "charged_amount": "0.361564", "collected_liability_amount": "0.361564", "funding_debit_amount": "0.361564", "refunded_liability_amount": "0.000000", "funding_refund_amount": "0.000000", "outstanding_receivable": "0.000000", "final_actual_cost": "0.361564", "events": [ { "billing_event_id": "bevt_example_hold_0001", "task_id": "task_32ae2a5177b310d3bc1ec83a98f63889", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "event_type": "hold_created", "billing_component": "generation_output", "amount": "0.782453", "charge_amount": "0.000000", "refund_amount": "0.000000", "collected_liability_amount": "0.000000", "funding_debit_amount": "0.000000", "funding_refund_amount": "0.000000", "receivable_amount": "0.000000", "receivable_direction": null, "adjustment_direction": null, "audit_reason": null, "component_detail_code": null, "currency": "USD", "settlement_status": "reserved", "created_at": 1793167380 }, { "billing_event_id": "bevt_example_capture_0001", "task_id": "task_32ae2a5177b310d3bc1ec83a98f63889", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "event_type": "charge_captured", "billing_component": "generation_output", "amount": "0.361564", "charge_amount": "0.361564", "refund_amount": "0.000000", "collected_liability_amount": "0.361564", "funding_debit_amount": "0.361564", "funding_refund_amount": "0.000000", "receivable_amount": "0.000000", "receivable_direction": null, "adjustment_direction": null, "audit_reason": null, "component_detail_code": null, "currency": "USD", "settlement_status": "settled", "created_at": 1793167382 }, { "billing_event_id": "bevt_example_release_0001", "task_id": "task_32ae2a5177b310d3bc1ec83a98f63889", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "event_type": "hold_released", "billing_component": "generation_output", "amount": "0.420889", "charge_amount": "0.000000", "refund_amount": "0.000000", "collected_liability_amount": "0.000000", "funding_debit_amount": "0.000000", "funding_refund_amount": "0.000000", "receivable_amount": "0.000000", "receivable_direction": null, "adjustment_direction": null, "audit_reason": null, "component_detail_code": null, "currency": "USD", "settlement_status": "settled", "created_at": 1793167383 } ], "next_cursor": null, "reconciliation": null } ``` 400 ```json { "error": { "code": "invalid_request", "message": "request parameters are invalid" }, "trace_id": "trace_error_example" } ``` 401 ```json { "error": { "code": "authentication_failed", "message": "authentication failed" }, "trace_id": "trace_error_example" } ``` 404 ```json { "error": { "code": "task_not_found", "message": "task was not found" }, "trace_id": "trace_error_example" } ``` 429 ```json { "error": { "code": "rate_limited", "message": "request rate limit exceeded" }, "trace_id": "trace_error_example" } ``` 500 ```json { "error": { "code": "internal_error", "message": "internal server error" }, "trace_id": "trace_error_example" } ``` 503 ```json { "error": { "code": "dependency_unavailable", "message": "service is not ready" }, "trace_id": "trace_error_example" } ``` --- ## 素材对象 素材状态与安全元数据。active 时必须返回 size_bytes 和 sha256;图片和视频还必须返回 width、height,音频和视频还必须返回 duration_ms。图片不返回 duration_ms,音频不返回 width、height;active 不返回 failure_reason。unavailable 时必须返回 failure_reason,且不返回大小、摘要、尺寸或时长;processing 时这些元数据与 failure_reason 均不返回。 ### 属性 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `asset_id` | string | 必填 | 平台素材标识。 长度:36–36。 格式:ast- 加 32 位十六进制。 | | `status` | string | 必填 | 当前状态,具体含义见对应取值。 `processing`:处理中; `active`:素材已可用; `unavailable`:不可用; | | `object` | string | 必填 | 对象类型标识。 固定为:asset。 | | `media_type` | string | 必填 | 媒体类型。 `image`:图片; `audio`:音频; `video`:视频; | | `model` | string | 必填 | 本次使用的模型标识。 `dreamina-seedance-2-0-260128`:Seedance 2.0; `dreamina-seedance-2-0-fast-260128`:Seedance 2.0 Fast; `dreamina-seedance-2-0-mini-260615`:Seedance 2.0 Mini; | | `name` | string | 可选 | 可选显示名称;不含控制符、行段分隔符或双向文本控制符。 长度:1–64。 | | `created_at` | integer | 必填 | 创建时间,Unix 整数秒。 范围:≥ 0。 | | `updated_at` | integer | 必填 | 最后更新时间,Unix 整数秒。 范围:≥ 0。 | | `width` | integer | 可选 | status 为 active 且 media_type 为 image 或 video 时必须返回的像素宽度;音频、处理中或不可用时不返回。 范围:≥ 1。 | | `height` | integer | 可选 | status 为 active 且 media_type 为 image 或 video 时必须返回的像素高度;音频、处理中或不可用时不返回。 范围:≥ 1。 | | `duration_ms` | integer | 可选 | status 为 active 且 media_type 为 audio 或 video 时必须返回的测量时长,毫秒;图片、处理中或不可用时不返回。 范围:≥ 1。 | | `size_bytes` | integer | 可选 | status 为 active 时必须返回的存储大小,字节;处理中或不可用时不返回。 范围:≥ 1。 | | `sha256` | string | 可选 | status 为 active 时必须返回的实际存储字节 SHA-256 摘要;处理中或不可用时不返回。 长度:64–64。 格式:64 位十六进制。 | | `failure_reason` | string | 可选 | status 为 unavailable 时必须返回的不可用原因;处理中或可用时不返回。 `source_url_not_allowed`:来源地址不符合获取策略; `source_fetch_failed`:来源获取失败; `source_too_large`:来源文件过大; `media_type_mismatch`:媒体类型不匹配; `media_constraints_violated`:不符合媒体约束; `review_rejected`:素材审核未通过; `review_unavailable`:素材审核暂不可用; `internal_error`:服务未能完成请求; | ### 示例 ```json { "asset_id": "ast-11111111111111111111111111111111", "object": "asset", "media_type": "image", "model": "dreamina-seedance-2-0-260128", "name": "reference", "created_at": 1700000000, "updated_at": 1700000100, "status": "processing" } ``` 用「[查询素材状态](./api-docs-zh-api-assets-retrieve.md)」读取这个对象。 --- ## 登记素材 `POST /v1/assets` 需要 service.use 权限。从公开 HTTPS 地址登记图片、音频或视频,接受后异步获取并校验。幂等范围为团队与登记操作;相同请求重放原响应,不同请求拒绝。不支持素材输入的模型返回 invalid_request,且不创建素材、接入任务或额度预留。 - 在请求头 `Authorization: Bearer` 后面放 API 密钥;API 密钥需要 `service.use` 权限。 - 必须带 `Idempotency-Key`;相同请求重放原响应,不同请求被拒绝。 - 接受后异步获取并校验,按 `Location` 查询状态。 - 只有 `active` 素材可以在生成请求里用 `asset://` 引用。 ### 请求头 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `Authorization` | string | 必填 | Bearer 加一个空格和 API 密钥。缺失或无效时返回 401 authentication_failed。 | | `Content-Type` | string | 必填 | 请求体是 JSON:application/json。 | | `X-Request-ID` | string | 可选 | 你自己的请求标识。无效或重复的值会被替换,最终值在响应头 X-Request-ID 返回。 | | `Idempotency-Key` | string | 必填 | 每个新请求生成一个新值并保存,以字母或数字开头。结果不明时用同一个键和原请求体重试,会返回原响应;同一个键配不同的请求体返回 409 idempotency_conflict。键按团队区分,不按 API 密钥区分。 长度:8–128。 字符:A–Z a–z 0–9 . _ : -。 | ### 请求体 · application/json · 必填 请求体只接受下列字段,其他字段会被拒绝。 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `url` | string | 必填 | 公开 HTTPS 素材来源地址,UTF-8 编码最多 2048 字节,不得含用户信息;获取策略也拒绝不安全目的地。 长度:9–2048。 格式:https://。 | | `media_type` | string | 必填 | 媒体类型。 `image`:图片; `audio`:音频; `video`:视频; | | `model` | string | 必填 | 本次使用的模型标识。 `dreamina-seedance-2-0-260128`:Seedance 2.0; `dreamina-seedance-2-0-fast-260128`:Seedance 2.0 Fast; `dreamina-seedance-2-0-mini-260615`:Seedance 2.0 Mini; | | `name` | string | 可选 | 可选显示名称;不含控制符、行段分隔符或双向文本控制符。 长度:1–64。 | ### 返回 · HTTP 202 素材接入已接受,请按 Location 查询状态。 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `asset_id` | string | 必填 | 平台素材标识。 长度:36–36。 格式:ast- 加 32 位十六进制。 | | `status` | string | 必填 | 当前状态,具体含义见对应取值。 固定为:processing。 | #### 响应头 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `X-Request-ID` | string | 必填 | 本次请求的标识:你提供的有效值,或替换后的值。 | | `Cache-Control` | string | 必填 | 响应不应被缓存。 | | `Location` | string | 必填 | 已接受对象的规范查询路径。 | ### 状态码 每个 `error.code` 的处理办法见「[错误码](./api-docs-zh-api-errors.md)」。 | 状态码 | 说明 | error.code | | --- | --- | --- | | 202 | 素材接入已接受,请按 Location 查询状态。 响应头:`Cache-Control`, `Location` | | | 400 | 请求无效,或素材来源地址不符合 HTTPS 获取策略。 | invalid_request, source_url_not_allowed | | 401 | API 密钥缺失或无效。 响应头:`WWW-Authenticate` | authentication_failed | | 403 | 当前 API Key 缺少 service.use 权限。 | permission_denied | | 409 | 本团队该操作键已用于不同的规范素材登记请求。 | idempotency_conflict | | 410 | 原接受操作的精确重放响应已过期,不会再次创建。 | idempotency_replay_expired | | 429 | 团队存储字节额度或素材接入容量不允许接受此素材。 | asset_intake_rate_limit_exceeded, asset_storage_quota_exceeded | | 503 | 服务暂时不可用。 | dependency_unavailable | ### 相关指南 - [上传素材](./api-docs-zh-assets.md) - [幂等与重试](./api-docs-zh-idempotency.md) ### 请求示例 ```bash IDEMPOTENCY_KEY=${IDEMPOTENCY_KEY:-$(uuidgen)} printf '%s\n' "保存 IDEMPOTENCY_KEY,重试时原样复用:${IDEMPOTENCY_KEY}" >&2 curl --request POST "${CHENVOYA_BASE_URL}/v1/assets" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" \ --header "Idempotency-Key: ${IDEMPOTENCY_KEY}" \ --header 'Content-Type: application/json' \ --data '{ "url": "https://images.example.test/reference.png", "media_type": "image", "model": "dreamina-seedance-2-0-260128", "name": "reference" }' ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} idempotency = os.environ.get("IDEMPOTENCY_KEY") or str(uuid.uuid4()) print("保存 IDEMPOTENCY_KEY,重试时原样复用:" + idempotency, file=sys.stderr) headers["Idempotency-Key"] = idempotency path = "/v1/assets" body_text = "{\"url\":\"https://images.example.test/reference.png\",\"media_type\":\"image\",\"model\":\"dreamina-seedance-2-0-260128\",\"name\":\"reference\"}" headers["Content-Type"] = "application/json" body = json.dumps(json.loads(body_text)).encode("utf-8") request = urllib.request.Request(base + path, data=body, headers=headers, method="POST") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const idempotency = process.env.IDEMPOTENCY_KEY || crypto.randomUUID(); console.error("保存 IDEMPOTENCY_KEY,重试时原样复用:" + idempotency); headers["Idempotency-Key"] = idempotency; const path = "/v1/assets"; let bodyText = "{\"url\":\"https://images.example.test/reference.png\",\"media_type\":\"image\",\"model\":\"dreamina-seedance-2-0-260128\",\"name\":\"reference\"}"; headers["Content-Type"] = "application/json"; const body = JSON.stringify(JSON.parse(bodyText)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "POST", headers, body: body}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "crypto/rand" "fmt" "io" "net/http" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } path := "/v1/assets" bodyText := "{\"url\":\"https://images.example.test/reference.png\",\"media_type\":\"image\",\"model\":\"dreamina-seedance-2-0-260128\",\"name\":\"reference\"}" request, err := http.NewRequest("POST", base+path, strings.NewReader(bodyText)) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) request.Header.Set("Content-Type", "application/json") idempotency := os.Getenv("IDEMPOTENCY_KEY") if idempotency == "" { idempotency = newOperationKey() } fmt.Fprintln(os.Stderr, "保存 IDEMPOTENCY_KEY,重试时原样复用:"+idempotency) request.Header.Set("Idempotency-Key", idempotency) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } // newOperationKey returns a random UUID; save it to retry with the same value. func newOperationKey() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { fail(err.Error()) } b[6] = b[6]&0x0f | 0x40 b[8] = b[8]&0x3f | 0x80 return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } ``` ### 响应示例 202 ```json { "asset_id": "ast-11111111111111111111111111111111", "status": "processing" } ``` 400 ```json { "error": { "code": "invalid_request", "message": "invalid asset request" }, "trace_id": "trace_asset_invalid" } ``` 401 ```json { "error": { "code": "authentication_failed", "message": "authentication failed" }, "trace_id": "trace_error_example" } ``` 403 ```json { "error": { "code": "permission_denied", "message": "permission denied" }, "trace_id": "trace_asset_permission" } ``` 409 ```json { "error": { "code": "idempotency_conflict", "message": "idempotency key conflicts with the original request" }, "trace_id": "trace_asset_conflict" } ``` 410 ```json { "error": { "code": "idempotency_replay_expired", "message": "asset create replay expired" }, "trace_id": "trace_asset_expired" } ``` 429 ```json { "error": { "code": "asset_storage_quota_exceeded", "message": "asset storage quota exceeded" }, "trace_id": "trace_asset_quota" } ``` 503 ```json { "error": { "code": "dependency_unavailable", "message": "service is not ready" }, "trace_id": "trace_error_example" } ``` --- ## 查询素材状态 `GET /v1/assets/{asset_id}` 需要 service.use 权限,仅可查询本团队素材。未知、已删除或其他团队素材统一返回 asset_not_found。活跃素材可返回尺寸、大小及摘要;音视频还返回时长。处理或不可用时隐藏尺寸、大小和摘要,不返回来源地址。 - 在请求头 `Authorization: Bearer` 后面放 API 密钥;API 密钥需要 `service.use` 权限。 - 只能查询本团队的素材;其他标识一律返回 `asset_not_found`。 - `active` 时返回尺寸、大小和摘要;`unavailable` 时查看 `failure_reason`。 ### 请求头 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `Authorization` | string | 必填 | Bearer 加一个空格和 API 密钥。缺失或无效时返回 401 authentication_failed。 | | `X-Request-ID` | string | 可选 | 你自己的请求标识。无效或重复的值会被替换,最终值在响应头 X-Request-ID 返回。 | ### 路径参数 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `asset_id` | string | 必填 | 平台素材标识,归属范围为认证团队。 长度:36–36。 格式:ast- 加 32 位十六进制。 | ### 返回 · HTTP 200 当前素材状态与可用的安全元数据。 返回「[素材对象](./api-docs-zh-api-assets-object.md)」,字段说明见该页。 #### 响应头 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `X-Request-ID` | string | 必填 | 本次请求的标识:你提供的有效值,或替换后的值。 | | `Cache-Control` | string | 必填 | 响应不应被缓存。 | ### 状态码 每个 `error.code` 的处理办法见「[错误码](./api-docs-zh-api-errors.md)」。 | 状态码 | 说明 | error.code | | --- | --- | --- | | 200 | 当前素材状态与可用的安全元数据。 响应头:`Cache-Control` | | | 400 | 请求无效。 | invalid_request | | 401 | API 密钥缺失或无效。 响应头:`WWW-Authenticate` | authentication_failed | | 403 | 当前 API Key 缺少 service.use 权限。 | permission_denied | | 404 | 本团队没有可见素材;未知、已删除及其他团队标识返回相同结果。 | asset_not_found | | 503 | 服务暂时不可用。 | dependency_unavailable | ### 相关指南 - [上传素材](./api-docs-zh-assets.md) - [生成视频](./api-docs-zh-video-generate.md) ### 请求示例 ```bash curl --request GET "${CHENVOYA_BASE_URL}/v1/assets/${ASSET_ID}" \ --header "Authorization: Bearer ${CHENVOYA_API_KEY}" ``` ```python import urllib.request import urllib.parse import json import os import sys import re import uuid key = os.environ["CHENVOYA_API_KEY"].strip() if not key or re.search(r"[\x00-\x1f\x7f-\x9f]", key): sys.exit("Set CHENVOYA_API_KEY to a non-empty value without control characters") base = os.environ["CHENVOYA_BASE_URL"].rstrip("/") headers = {"Authorization": "Bearer " + key} ASSET_ID = os.environ["ASSET_ID"] if not re.fullmatch("^ast-[0-9a-f]{32}$", ASSET_ID): sys.exit("Set ASSET_ID to the ID from the creation response") path = "/v1/assets/{asset_id}".replace("{asset_id}", urllib.parse.quote(ASSET_ID, safe="")) request = urllib.request.Request(base + path, data=None, headers=headers, method="GET") try: with urllib.request.urlopen(request) as response: print(response.read().decode("utf-8")) except urllib.request.HTTPError as error: print(error.read().decode("utf-8")) sys.exit(1) ``` ```javascript // 需要 Node.js 20 及以上版本,保存为 .mjs 文件后运行。 const key = process.env.CHENVOYA_API_KEY?.trim(); if (!key || /[\x00-\x1f\x7f-\x9f]/.test(key)) { console.error("Set CHENVOYA_API_KEY to a non-empty value without control characters"); process.exit(1); } const base = process.env.CHENVOYA_BASE_URL; if (!base) throw new Error("Set CHENVOYA_BASE_URL"); const headers = {Authorization: `Bearer ${key}`}; const ASSET_ID = process.env.ASSET_ID; if (!ASSET_ID) throw new Error("Set ASSET_ID"); if (typeof ASSET_ID !== "string" || (new RegExp("^ast-[0-9a-f]{32}$")).exec(ASSET_ID)?.[0] !== ASSET_ID) throw new Error("Set ASSET_ID to the ID from the creation response"); const path = "/v1/assets/{asset_id}".replace("{asset_id}", encodeURIComponent(ASSET_ID)); const response = await fetch(base.replace(/\/$/, "") + path, {method: "GET", headers, body: undefined}); console.log(await response.text()); if (!response.ok) process.exitCode = 1; ``` ```go package main import ( "fmt" "io" "net/http" "net/url" "os" "regexp" "strings" ) func main() { key := strings.TrimSpace(os.Getenv("CHENVOYA_API_KEY")) if key == "" || regexp.MustCompile(`[\x00-\x1f\x7f-\x9f]`).MatchString(key) { fail("Set CHENVOYA_API_KEY to a non-empty value without control characters") } base := strings.TrimRight(os.Getenv("CHENVOYA_BASE_URL"), "/") if base == "" { fail("Set CHENVOYA_BASE_URL") } assetID := os.Getenv("ASSET_ID") if assetID == "" { fail("Set ASSET_ID") } if !regexp.MustCompile(`\A(?:ast-[0-9a-f]{32})\z`).MatchString(assetID) { fail("Set ASSET_ID to the ID from the creation response") } path := strings.Replace("/v1/assets/{asset_id}", "{asset_id}", url.PathEscape(assetID), 1) request, err := http.NewRequest("GET", base+path, nil) if err != nil { fail(err.Error()) } request.Header.Set("Authorization", "Bearer "+key) response, err := http.DefaultClient.Do(request) if err != nil { fail(err.Error()) } text, err := io.ReadAll(response.Body) response.Body.Close() if err != nil { fail(err.Error()) } fmt.Println(string(text)) if response.StatusCode < 200 || response.StatusCode > 299 { os.Exit(1) } } func fail(message string) { fmt.Fprintln(os.Stderr, message) os.Exit(1) } ``` ### 响应示例 200 ```json { "asset_id": "ast-11111111111111111111111111111111", "object": "asset", "media_type": "image", "model": "dreamina-seedance-2-0-260128", "name": "reference", "created_at": 1700000000, "updated_at": 1700000100, "status": "processing" } ``` 400 ```json { "error": { "code": "invalid_request", "message": "request parameters are invalid" }, "trace_id": "trace_error_example" } ``` 401 ```json { "error": { "code": "authentication_failed", "message": "authentication failed" }, "trace_id": "trace_error_example" } ``` 403 ```json { "error": { "code": "permission_denied", "message": "permission denied" }, "trace_id": "trace_asset_permission" } ``` 404 ```json { "error": { "code": "asset_not_found", "message": "asset not found" }, "trace_id": "trace_asset_missing" } ``` 503 ```json { "error": { "code": "dependency_unavailable", "message": "service is not ready" }, "trace_id": "trace_error_example" } ``` --- ## 接收任务完成通知 `POST task.completed` 任务进入成功、失败或取消终态后发送 task.completed;成功任务须已完成交付处理。通知为至少送达一次,同一事件重试保持标识与正文原始字节不变。通知不包含结果下载地址,最新状态与结果请查询任务。验签、去重及重试规则见回调通知总览。 - 返回任意 2xx 表示已收到;其他状态或超时会按计划重试。 - 先用 `Chenvoya-Signature` 验证签名,再处理正文。 - 至少送达一次:验签后按正文的 `event_id` 去重。 - 通知不含下载地址;结果请查询任务。 ### 请求头 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `Chenvoya-Event-Id` | string | 必填 | 事件标识的便利副本,此请求头未被签名覆盖。验签后以正文 event_id 去重。 格式:uuid。 | | `Chenvoya-Signature` | string | 必填 | 通知签名,包含 t 与 v1。按回调通知总览校验时间戳,并使用原始正文计算 HMAC-SHA256 后做常量时间比较。 | ### 请求体 · application/json · 必填 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `event_id` | string | 必填 | 已签名的事件唯一标识,接收方验签成功后使用此字段去重。 格式:uuid。 | | `object` | string | 必填 | 通知对象类型,固定为 event。 固定为:event。 | | `type` | string | 必填 | 通知类型,区分任务完成与任务结算。 `task.completed`:任务完成通知; `task.billing_settled`:任务结算通知; | | `schema_version` | integer | 必填 | 通知正文格式版本,当前为整数一。 固定为:1。 | | `created_at` | integer | 必填 | 事件创建时间,Unix 整数秒;与每次尝试的签名时间分别校验。 范围:≥ 0。 | | `data` | object | 必填 | 事件冻结时的客户任务快照。 | | `data.task_id` | string | 必填 | 本团队的平台任务标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `data.request_id` | string | 必填 | 创建任务的请求关联标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `data.billing_correlation_id` | string | 必填 | 创建任务的费用关联标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `data.model` | string | 必填 | 本任务的公开模型标识。 `dreamina-seedance-2-0-260128`:Seedance 2.0; `dreamina-seedance-2-0-fast-260128`:Seedance 2.0 Fast; `dreamina-seedance-2-0-mini-260615`:Seedance 2.0 Mini; | | `data.status` | string | 必填 | 任务终态,区分成功、失败与取消。 `succeeded`:已成功; `failed`:已失败; `cancelled`:已取消; | | `data.delivery_status` | string | 必填 | 事件冻结时的结果交付状态。 `pending`:待交付; `materializing`:准备下载; `available`:可下载; `output_unavailable`:结果不可用; | | `data.error` | object | 必填,可能为空 | 任务错误对象;没有错误时为空。 | | `data.error.code` | string | 必填 | 机器可读错误码。 `no_common_asset_route`:素材暂不可用; `asset_not_ready`:引用素材仍在处理中; `asset_review_timeout`:素材审核超时; `route_misconfigured`:服务配置错误; `route_temporarily_unavailable`:暂不可用; `generation_expired`:生成已超时; `generation_failed`:生成失败; `input_fetch_failed`:输入素材下载失败; `input_url_not_allowed`:输入素材链接不允许下载; `input_too_large`:输入素材超过大小上限; `input_unsupported`:输入素材格式、尺寸或时长不受支持; `input_rejected`:输入素材未通过内容审核; `input_preparation_failed`:未能及时准备输入素材; `input_duration_exceeded`:输入素材总时长超过型号上限; | | `data.error.message` | string | 必填 | 供人阅读的错误说明。 长度:1–1024。 | | `data.error.details` | object | 可选 | 可选的结构化错误详情。 | | `data.error.details.content_index` | integer | 可选 | 失败输入在原始 content 数组中的下标,从 0 起,文字项也计入。 范围:0–50。 | | `data.settlement_status` | string | 必填 | 事件冻结时的费用结算状态。 `reserved`:已预留费用; `pending`:待处理; `reconciliation_required`:需要核对; `settled`:已结算; `settled_with_receivable`:已结算,仍有应收金额; | | `data.final_actual_cost` | string | 必填,可能为空 | 最终实际费用,单位为美元并保留六位小数;尚未确定时为空。 格式:6 位小数。 | ### 响应 接收成功。任意 2xx 响应均确认送达,其他状态或超时将按计划重试。 #### 重试计划 - 10 秒后 - 1 分钟后 - 5 分钟后 - 之后每小时一次 - 最长 7 天 #### 验证签名 验签、去重和时间窗口的完整规则见「[验证回调签名](./api-docs-zh-callbacks-verify.md)」。 ```python import base64 import hashlib import hmac import time def verify(signing_secret, header, body): fields = {} for item in header.split(","): name, _, value = item.strip().partition("=") fields[name] = value t, v1 = fields.get("t", ""), fields.get("v1", "") if not t.isdigit() or abs(time.time() - int(t)) > 300: return False key = base64.urlsafe_b64decode(signing_secret + "=" * (-len(signing_secret) % 4)) signed = b"chenvoya.v1." + t.encode() + b"." + body expected = hmac.new(key, signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, v1) ``` ### 相关指南 - [接收回调](./api-docs-zh-callbacks.md) - [验证回调签名](./api-docs-zh-callbacks-verify.md) ### 请求体示例 ```json { "event_id": "7f9c2ba4-e88f-4d2b-9a2f-0c3e5b8d1a64", "object": "event", "type": "task.completed", "schema_version": 1, "created_at": 1793167400, "data": { "task_id": "task_32ae2a5177b310d3bc1ec83a98f63889", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "model": "dreamina-seedance-2-0-260128", "status": "succeeded", "delivery_status": "available", "error": null, "settlement_status": "pending", "final_actual_cost": null } } ``` --- ## 接收任务结算通知 `POST task.billing_settled` task.completed 已送达或放弃且任务结算为 settled 或 settled_with_receivable 后,发送 task.billing_settled,使用独立的七天投递窗口。完成三十天后仍未结算的任务不再发送结算通知。正文中的最终实际费用与查询任务的投影一致,失败或取消且已结算的任务为零。 - 返回任意 2xx 表示已收到;其他状态或超时会按计划重试。 - 签名、去重和重试规则与 `task.completed` 相同。 - 在 `task.completed` 送达或放弃、且结算确定后发送。 - 完成三十天后仍未结算的任务不再发送这条通知。 ### 请求头 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `Chenvoya-Event-Id` | string | 必填 | 事件标识的便利副本,此请求头未被签名覆盖。验签后以正文 event_id 去重。 格式:uuid。 | | `Chenvoya-Signature` | string | 必填 | 通知签名,包含 t 与 v1。按回调通知总览校验时间戳,并使用原始正文计算 HMAC-SHA256 后做常量时间比较。 | ### 请求体 · application/json · 必填 | 字段 | 类型 | 要求 | 说明 | | --- | --- | --- | --- | | `event_id` | string | 必填 | 已签名的事件唯一标识,接收方验签成功后使用此字段去重。 格式:uuid。 | | `object` | string | 必填 | 通知对象类型,固定为 event。 固定为:event。 | | `type` | string | 必填 | 通知类型,区分任务完成与任务结算。 `task.completed`:任务完成通知; `task.billing_settled`:任务结算通知; | | `schema_version` | integer | 必填 | 通知正文格式版本,当前为整数一。 固定为:1。 | | `created_at` | integer | 必填 | 事件创建时间,Unix 整数秒;与每次尝试的签名时间分别校验。 范围:≥ 0。 | | `data` | object | 必填 | 事件冻结时的客户任务快照。 | | `data.task_id` | string | 必填 | 本团队的平台任务标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `data.request_id` | string | 必填 | 创建任务的请求关联标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `data.billing_correlation_id` | string | 必填 | 创建任务的费用关联标识。 长度:4–128。 字符:A–Z a–z 0–9 _ -。 | | `data.model` | string | 必填 | 本任务的公开模型标识。 `dreamina-seedance-2-0-260128`:Seedance 2.0; `dreamina-seedance-2-0-fast-260128`:Seedance 2.0 Fast; `dreamina-seedance-2-0-mini-260615`:Seedance 2.0 Mini; | | `data.status` | string | 必填 | 任务终态,区分成功、失败与取消。 `succeeded`:已成功; `failed`:已失败; `cancelled`:已取消; | | `data.delivery_status` | string | 必填 | 事件冻结时的结果交付状态。 `pending`:待交付; `materializing`:准备下载; `available`:可下载; `output_unavailable`:结果不可用; | | `data.error` | object | 必填,可能为空 | 任务错误对象;没有错误时为空。 | | `data.error.code` | string | 必填 | 机器可读错误码。 `no_common_asset_route`:素材暂不可用; `asset_not_ready`:引用素材仍在处理中; `asset_review_timeout`:素材审核超时; `route_misconfigured`:服务配置错误; `route_temporarily_unavailable`:暂不可用; `generation_expired`:生成已超时; `generation_failed`:生成失败; `input_fetch_failed`:输入素材下载失败; `input_url_not_allowed`:输入素材链接不允许下载; `input_too_large`:输入素材超过大小上限; `input_unsupported`:输入素材格式、尺寸或时长不受支持; `input_rejected`:输入素材未通过内容审核; `input_preparation_failed`:未能及时准备输入素材; `input_duration_exceeded`:输入素材总时长超过型号上限; | | `data.error.message` | string | 必填 | 供人阅读的错误说明。 长度:1–1024。 | | `data.error.details` | object | 可选 | 可选的结构化错误详情。 | | `data.error.details.content_index` | integer | 可选 | 失败输入在原始 content 数组中的下标,从 0 起,文字项也计入。 范围:0–50。 | | `data.settlement_status` | string | 必填 | 事件冻结时的费用结算状态。 `reserved`:已预留费用; `pending`:待处理; `reconciliation_required`:需要核对; `settled`:已结算; `settled_with_receivable`:已结算,仍有应收金额; | | `data.final_actual_cost` | string | 必填,可能为空 | 最终实际费用,单位为美元并保留六位小数;尚未确定时为空。 格式:6 位小数。 | ### 响应 接收成功。任意 2xx 响应均确认送达,其他状态或超时将按计划重试。 #### 重试计划 - 10 秒后 - 1 分钟后 - 5 分钟后 - 之后每小时一次 - 最长 7 天 #### 验证签名 验签、去重和时间窗口的完整规则见「[验证回调签名](./api-docs-zh-callbacks-verify.md)」。 ```python import base64 import hashlib import hmac import time def verify(signing_secret, header, body): fields = {} for item in header.split(","): name, _, value = item.strip().partition("=") fields[name] = value t, v1 = fields.get("t", ""), fields.get("v1", "") if not t.isdigit() or abs(time.time() - int(t)) > 300: return False key = base64.urlsafe_b64decode(signing_secret + "=" * (-len(signing_secret) % 4)) signed = b"chenvoya.v1." + t.encode() + b"." + body expected = hmac.new(key, signed, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, v1) ``` ### 相关指南 - [接收回调](./api-docs-zh-callbacks.md) - [计费方式](./api-docs-zh-billing.md) ### 请求体示例 ```json { "event_id": "7f9c2ba4-e88f-4d2b-9a2f-0c3e5b8d1a64", "object": "event", "type": "task.billing_settled", "schema_version": 1, "created_at": 1793167400, "data": { "task_id": "task_32ae2a5177b310d3bc1ec83a98f63889", "request_id": "req_6acd96894df9798feb0ddb21e5a52d70", "billing_correlation_id": "billcorr_2f6c57065a4404b27fb9b6b24ea9a0d0", "model": "dreamina-seedance-2-0-260128", "status": "succeeded", "delivery_status": "available", "error": null, "settlement_status": "settled", "final_actual_cost": "0.361564" } } ``` --- ## 错误码 每个 error.code 的含义、状态码和出现的接口。 | 状态码 | error.code | 含义 | 怎么处理 | 能否原样重试 | 是否收费 | | --- | --- | --- | --- | --- | --- | | 400, 404 | `asset_not_found` | 素材不存在、已删除或不属于本团队(出现于查询素材状态、创建视频任务) | 检查素材 ID 是否属于本团队且未删除。 | 不可以 | 不收费 | | 400 | `asset_unusable` | 素材失败、媒体类型不适用或审核不适用于当前请求(出现于创建视频任务) | 换一个可用的素材,或让素材类型与用途一致。 | 不可以 | 不收费 | | 400 | `callback_target_not_verified` | 请求地址与本团队已验证、未撤销且未过期的回调目标地址不逐字节相同(出现于创建视频任务) | 在控制台登记并验证这个回调地址,或去掉 `callback_url`。 | 不可以 | 不收费 | | 400 | `input_video_duration_exceeded` | 输入视频总时长超过模型限制(出现于创建视频任务) | 缩短参考视频,使总时长不超过模型上限。 | 不可以 | 不收费 | | 400 | `invalid_request` | 请求无效,包含超过模型参考数量的情况(出现于登记素材、查询素材状态、创建视频任务、列出团队任务、查询视频任务、取消视频任务、查询单任务账单) | 按 `error.message` 修正请求后再发送。 | 不可以 | 不收费 | | 400 | `source_url_not_allowed` | 来源地址不符合获取策略(出现于登记素材) | 换一个符合要求的公开 HTTPS 地址。 | 不可以 | 不收费 | | 400 | `unsupported_capability` | 可用选项无法同时满足所需能力(出现于创建视频任务) | 按 `error.details` 里的参数调整请求。 | 不可以 | 不收费 | | 401 | `authentication_failed` | 认证失败(出现于登记素材、查询素材状态、创建视频任务、列出团队任务、查询视频任务、取消视频任务、查询单任务账单) | 检查 `Authorization: Bearer` 后面的 API 密钥。 | 不可以 | 不收费 | | 403 | `permission_denied` | 缺少所需权限(出现于登记素材、查询素材状态) | 换一把有 `service.use` 权限的 API 密钥。 | 不可以 | 不收费 | | 404 | `task_not_found` | 任务不存在或对本团队不可见(出现于查询视频任务、取消视频任务、查询单任务账单) | 检查任务 ID 是否属于本团队。 | 不可以 | 不收费 | | 409 | `asset_not_ready` | 引用素材仍在处理中(出现于创建视频任务) | 等素材变为 `active` 后,用同一个键原样重试。 | 条件满足后可以 | 不收费 | | 409 | `idempotency_conflict` | 同一个幂等键用于不同请求(出现于登记素材、创建视频任务、取消视频任务) | 用原请求体重试同一个键;新请求换一个新键。 | 不可以 | 不收费 | | 409, 410 | `idempotency_replay_expired` | 精确重放响应已过期(出现于登记素材、创建视频任务、取消视频任务) | 原请求已被接受,按 ID 查询它,不要换键重发。 | 不可以 | 不收费 | | 409 | `key_provisioning_incomplete` | 该 API Key 的额度配置未完成,需删除后新建(出现于创建视频任务) | 删除这把 API 密钥,再新建一把。 | 不可以 | 不收费 | | 409 | `task_not_cancellable` | 终态任务没有可引用的取消操作(出现于取消视频任务) | 任务已结束,无需取消。 | 不可以 | 不收费 | | 429 | `asset_intake_rate_limit_exceeded` | 素材接入容量受限(出现于登记素材) | 稍后用同一个键重试。 | 条件满足后可以 | 不收费 | | 429 | `asset_storage_quota_exceeded` | 素材存储额度不足(出现于登记素材、创建视频任务) | 存储额度不足,清理素材或联系我们后再登记。 | 不可以 | 不收费 | | 429 | `insufficient_balance` | 余额不足以接受请求(出现于创建视频任务) | 充值后用同一个键重试;`error.details` 给出所需的预留金额。 | 条件满足后可以 | 不收费 | | 429 | `rate_limited` | 请求频率受限(出现于创建视频任务、列出团队任务、查询视频任务、取消视频任务、查询单任务账单) | 等 `Retry-After` 秒后用同一个键重试。 | 条件满足后可以 | 不收费 | | 500 | `internal_error` | 服务未能完成请求(出现于创建视频任务、列出团队任务、查询视频任务、取消视频任务、查询单任务账单) | 用同一个 `Idempotency-Key` 原样重试。 | 可以,用同一个键 | 不收费;重试不会重复收费 | | 503 | `dependency_unavailable` | 服务暂时不可用,结果不确定(出现于登记素材、查询素材状态、创建视频任务、列出团队任务、查询视频任务、取消视频任务、查询单任务账单) | 结果不确定,用同一个键原样重试。 | 可以,用同一个键 | 不收费;重试不会重复收费 | | 503 | `funding_intake_closed` | 新接入已关闭(出现于创建视频任务) | 暂不接受新任务,稍后再试;已接受请求的原样重放仍返回原响应。 | 条件满足后可以 | 不收费 | --- ## 状态与原因 任务、交付、结算、素材和账单的全部取值。 ### 任务状态 status | 取值 | 含义 | 是否终态 | | --- | --- | --- | | `queued` | 排队中 | 否 | | `processing` | 处理中 | 否 | | `succeeded` | 已成功 | 是 | | `failed` | 已失败 | 是 | | `cancelled` | 已取消 | 是 | ### 执行阶段 phase | 取值 | 含义 | | --- | --- | | `preparing_assets` | 准备素材 | | `waiting_route` | 等待处理 | | `submitting` | 准备生成 | | `provider_processing` | 生成中 | | `reconciling` | 确认结果中 | | `finalizing` | 整理结果 | | `settlement_pending` | 等待结算 | ### 交付状态 delivery_status | 取值 | 含义 | | --- | --- | | `pending` | 待交付 | | `materializing` | 准备下载 | | `available` | 可下载 | | `output_unavailable` | 结果不可用 | ### 结算状态 settlement_status | 取值 | 含义 | | --- | --- | | `reserved` | 已预留费用 | | `pending` | 待处理 | | `reconciliation_required` | 需要核对 | | `settled` | 已结算 | | `settled_with_receivable` | 已结算,仍有应收金额 | ### 任务错误码 error.code | 取值 | 含义 | | --- | --- | | `no_common_asset_route` | 素材暂不可用 | | `asset_not_ready` | 引用素材仍在处理中 | | `asset_review_timeout` | 素材审核超时 | | `route_misconfigured` | 服务配置错误 | | `route_temporarily_unavailable` | 暂不可用 | | `generation_expired` | 生成已超时 | | `generation_failed` | 生成失败 | | `input_fetch_failed` | 输入素材下载失败 | | `input_url_not_allowed` | 输入素材链接不允许下载 | | `input_too_large` | 输入素材超过大小上限 | | `input_unsupported` | 输入素材格式、尺寸或时长不受支持 | | `input_rejected` | 输入素材未通过内容审核 | | `input_preparation_failed` | 未能及时准备输入素材 | | `input_duration_exceeded` | 输入素材总时长超过型号上限 | ### 素材状态 status | 取值 | 含义 | | --- | --- | | `processing` | 处理中 | | `active` | 素材已可用 | | `unavailable` | 不可用 | ### 素材不可用原因 failure_reason | 取值 | 含义 | | --- | --- | | `source_url_not_allowed` | 来源地址不符合获取策略 | | `source_fetch_failed` | 来源获取失败 | | `source_too_large` | 来源文件过大 | | `media_type_mismatch` | 媒体类型不匹配 | | `media_constraints_violated` | 不符合媒体约束 | | `review_rejected` | 素材审核未通过 | | `review_unavailable` | 素材审核暂不可用 | | `internal_error` | 服务未能完成请求 | ### 取消结果 cancel_status | 取值 | 含义 | | --- | --- | | `requested` | 已请求取消 | | `effective` | 取消已生效 | | `not_effective` | 取消未生效 | | `failed` | 失败 | ### 账单事件 event_type | 取值 | 含义 | | --- | --- | | `hold_created` | 创建费用预留 | | `hold_increased` | 增加费用预留 | | `hold_reduced` | 减少费用预留 | | `charge_captured` | 收取费用 | | `supplemental_charge` | 补充计费 | | `receivable_created` | 新增应收 | | `receivable_payment` | 支付应收 | | `receivable_reduced` | 减少应收 | | `hold_released` | 释放费用预留 | | `refund_posted` | 退款已记账 | | `charge_waived` | 费用已免除 | | `manual_adjustment` | 人工调整 |