04 回调 (Webhook)
创建任务时传
callback_url,任务在 queued → running → succeeded 三个状态时, 上游官方 会向你的 URL POST 任务对象。 替代轮询。
当前状态
回调功能 已开通(2026-05-27 起)。 上游官方 直接向你提供的 callback_url POST,不经过本网关二次转发。 平台日志已观察到 queued / running / succeeded 三次回调正常送达。
使用方法
Step 1: 准备一个可公网访问的 endpoint
接收回调的服务端 endpoint 需要:
- HTTPS (推荐), HTTP 也可工作但不安全
- 公网可达(域名解析正确, 端口开放)
- 接收 POST +
application/json - 响应 2xx 表示成功 — 任何 4xx / 5xx 上游会重试
最简 Node.js 示例:
import express from 'express'
const app = express()
app.use(express.json({ limit: '1mb' }))
app.post('/seedance/callback', (req, res) => {
const body = req.body
console.log('Got callback:', body.data.id, body.data.status)
// 处理: 如果是 succeeded, 立刻下载视频 (24h 过期)
res.json({ ok: true })
})
app.listen(8080)Step 2: 创建任务时附 callback_url
curl -X POST https://www.dianlitoken.com/v1/videos \
-H "Authorization: Bearer sk-xxx" \
-H "Content-Type: application/json" \
-d '{
"model": "seedance-2.0-fast",
"content": [{"type":"text","text":"猫在草地上跑"}],
"resolution": "480p",
"duration": 4,
"callback_url": "https://your-server.com/seedance/callback"
}'任务进入终态前 (queued / running / succeeded), 上游会向 callback_url POST 三次。
真实回调 Payload (重要)
所有回调都用这个 envelope 包裹, 顶层是 {uid, callbackType, callbackUrl, data}:
{
"uid": 1000172,
"callbackType": "ANT_VIDEO",
"callbackUrl": "https://your-server.com/seedance/callback",
"data": {
"id": "cgt-20260527165600-kf7rt",
"model": "ant-2-fast-text-2-video",
"status": "succeeded",
"content": {
"video_url": "https://ark-aigc-cn-beijing.tos-cn-beijing.volces.com/transfer/2026/05/27/177987227916546410508.mp4?X-Tos-..."
},
"usage": {
"completion_tokens": 40594,
"total_tokens": 40594
},
"created_at": 1779872160,
"updated_at": 1779872279,
"seed": 38421,
"resolution": "480p",
"ratio": "16:9",
"duration": 4,
"framespersecond": 24,
"service_tier": "default",
"execution_expires_after": 172800,
"generate_audio": true,
"draft": false,
"priority": 0,
"extra": {
"pre_money": "50.00",
"money": "1.5019780",
"pre_ai_coins": 0,
"ai_coins": 0,
"cost_time": 119
}
}
}字段说明 (data 子对象)
| 字段 | 类型 | 何时存在 | 说明 |
|---|---|---|---|
id | string | 总是 | 任务 ID, 格式 cgt-yyyymmddhhmmss-xxxxx。 跟创建任务 / 查询接口返的 id 完全一致 |
model | string | 总是 | 上游真实模型名 (ant-2-*), 不是 客户端传的 seedance-2.0-* 别名 |
status | string | 总是 | queued / running / succeeded / failed / cancelled / expired — 上游用的是这套 Volcengine 风格枚举 |
content.video_url | string | 仅 succeeded | 视频下载 URL, 24h 过期 |
usage.total_tokens | int | 仅 succeeded | 实际消耗 token |
usage.completion_tokens | int | 仅 succeeded | 通常和 total_tokens 相同 |
created_at / updated_at | int64 | 总是 | Unix 秒 |
duration | int | 仅 succeeded | 实际生成时长 (秒) |
resolution | string | 仅 succeeded | 实际分辨率 |
ratio | string | 可选 | 宽高比 (如 16:9) |
framespersecond | int | 仅 succeeded | 帧率 |
seed | int64 | 仅 succeeded | 实际使用的种子 |
extra.money | string | 仅 succeeded | 上游真实计费金额 (CNY, 字符串) |
extra.cost_time | int | 仅 succeeded | 生成耗时 (秒) |
data.id 跟客户端 task ID 完全一致
回调里 data.id 跟你创建任务时拿到的 id 是同一个值(cgt-yyyymmddhhmmss-xxxxx 格式),可以直接做映射:创建任务时把 id 当主键存数据库,收到回调时按 data.id 直接 SELECT 找回原任务记录,不需要中间转换层。
也可以把 id 嵌入 callback_url 路径(如 https://x.com/cb/{id}),从 URL path 反查关联 — 适合无状态服务架构。
三次回调的时序
| 顺序 | 状态 | 含义 | 何时到达 |
|---|---|---|---|
| 1 | queued | 已排队, 还未开始 | 创建后约 1-2 秒 |
| 2 | running | 上游开始生成 | queued 后 0-30 秒 |
| 3 | succeeded / failed / expired / cancelled | 终态 | Fast: 1-2 分钟; Pro: 2-5 分钟 |
关键: 你只关心 succeeded 的那一次, 前两次只是状态变化, 没有视频。
app.post('/seedance/callback', (req, res) => {
const { id, status, content } = req.body.data
if (status === 'succeeded') {
// 立刻下载视频(24h URL 过期)
downloadVideo(id, content.video_url)
} else if (status === 'failed' || status === 'expired' || status === 'cancelled') {
// 通知用户失败 + 上游会全额退费
notifyUserTaskFailed(id, status)
}
// queued / running 一般不用做事
res.json({ ok: true })
})接收 endpoint 的健壮性要求
1. 必须返回 2xx 速度要快
回调 endpoint 应 < 1 秒返回 2xx。 慢 endpoint 会导致上游认为失败 + 重试 → 你会收到重复回调。
不要在 endpoint 里同步下载视频, 而是:
app.post('/seedance/callback', async (req, res) => {
// 立即 ack
res.json({ ok: true })
// 异步处理(下载 / 落库 / 通知用户)
if (req.body.data.status === 'succeeded') {
setImmediate(() => handleVideo(req.body.data))
}
})2. 必须幂等
同一个 task 可能因为网络重试收到 同状态的多次回调。 用 data.id + data.status 做幂等键:
const processed = new Set() // 生产环境用 Redis
function handleCallback(data) {
const key = `${data.id}:${data.status}`
if (processed.has(key)) return
processed.add(key)
// 实际处理
}3. 必须验证来源
任何人都能 POST 到你的 endpoint。 建议:
方案 A — 在 URL path 里嵌入秘密 token
https://your-server.com/seedance/cb/zZ-secret-string服务端只接受这个秘密 path 的 POST, 别的全 401。 简单有效, 推荐。
方案 B — 限定来源 IP
上游官方 的回调 IP 段(已抓到): 43.175.171.0/24, 43.174.168.0/24 (可能不完整, 以平台公告为准)。 网关侧加 firewall / nginx allowlist。
不推荐方案: 用 HMAC 签名 —— 上游官方 目前没有签发签名 header, 无法验证。
用 path-secret 模式 (推荐)
把 secret 嵌进 URL,无须签名:
# 启动时生成一个随机 secret 存起来
import secrets, os
CB_SECRET = os.environ.get('CB_SECRET') or secrets.token_urlsafe(32)
# 创建任务时把 secret 嵌入 callback_url
task = requests.post(
'https://www.dianlitoken.com/v1/videos',
headers={'Authorization': f'Bearer {KEY}'},
json={
'model': 'seedance-2.0-fast',
'content': [{'type':'text','text':'...'}],
'callback_url': f'https://your-server.com/cb/{CB_SECRET}',
},
).json()
# 服务端只接受 /cb/{CB_SECRET} 这一个路径
from flask import Flask, request, abort
app = Flask(__name__)
@app.post('/cb/<secret>')
def callback(secret):
if secret != CB_SECRET:
abort(401)
data = request.json['data']
# 处理 data...
return {'ok': True}失败重试 & 兜底策略
上游的回调送达不是 100% 可靠。 建议同时保留轮询作为兜底:
def process_task(task_id, callback_url):
# 用回调为主
requests.post(f'{BASE}/v1/videos', json={
'model': 'seedance-2.0-fast',
'content': [...],
'callback_url': callback_url,
})
# 仍然挂一个 30 分钟超时的兜底轮询
threading.Thread(target=fallback_poll, args=(task_id, 1800)).start()回调 + 轮询双重保险, 任何一个先到都按"已收到"处理(幂等键防重复)。
调试
用 curl 模拟回调测试 endpoint
# 测试 succeeded 回调
curl -X POST 'https://your-server.com/cb/your-secret' \
-H 'Content-Type: application/json' \
-d '{
"uid": 1000000,
"callbackType": "ANT_VIDEO",
"callbackUrl": "https://your-server.com/cb/your-secret",
"data": {
"id": "cgt-test-001",
"status": "succeeded",
"content": {"video_url": "https://example.com/test.mp4"},
"usage": {"total_tokens": 1000},
"duration": 4,
"resolution": "480p"
}
}'看不到回调?
排查清单:
| 现象 | 原因 | 修法 |
|---|---|---|
| 完全收不到 | callback_url 不公网可达 | curl 测试: 平台公网能否 curl -X POST 你的URL? |
| 收到 queued 但没 running/succeeded | endpoint 返回 5xx | 看你 endpoint 日志,响应 < 1s + 返回 2xx |
| 重复收到同一状态 | endpoint 太慢上游重试 | 缩短响应时间,异步处理 |
| 收到了但 body 不像我文档说的 | callbackType ≠ ANT_VIDEO? | 拒掉非 ANT_VIDEO 类型 |
仅靠回调够用吗?
不够。 回调可能丢, 上游对失败的重试策略有限。 建议:
- 关键业务 — 回调 + 30 分钟兜底轮询
- 非关键业务 — 单回调 + 用户主动刷新触发
GET /v1/videos/{task_id} - 离线批处理 — 直接用轮询, 不用回调(简单)
下一篇: 05 素材库 →