# 任务回调
> 在任务状态变化时接收通知,校验平台签名,安全去重,再按平台任务 ID 查询结果。
回调只提示状态变化,不能替代任务查询。提交后保存平台任务 ID;收到通知或长时间没有通知,都通过原查询接口确认产物、错误和账务状态。本页说明当前实现的通知契约,真实 Provider 与真实回调链路尚未验证,不代表公网投递已经验收可用。
## 哪些请求可以使用
| 接口 | 回调参数 | 通知中的任务 ID |
| --- | --- | --- |
| 平台图片、视频和 3D 生成 | 顶层 `callback_url` | 顶层 `id`,通过 `/media/v1/tasks/{id}` 查询 |
| Seedance 原生任务提交 | 顶层 `callback_url` | 顶层 `id`,通过原生任务查询接口查询 |
| MiniMax H3 原生任务提交 | 顶层 `callback_url` | 顶层 `task_id`,通过 H3 查询接口查询 |
| 可灵原生视频提交 | `options.callback_url` | 顶层 `id`;保持该版本可灵任务包装 |
| 可灵主体、音色创建 | 顶层 `callback_url` | 顶层 `task_id`;资源接口使用 `task_status`,成功值为 `succeed` |
回调 URL 必须是你控制的 HTTPS 接收地址,默认使用公网 443 端口;不得含 URL 用户名、密码或片段,不接受跳转。确需内部地址或其他端口时,由管理员先批准对应网络范围。目标被禁止、解析失败或 H3 验证失败时,生成在发送给厂商之前被拒绝。
平台图片、视频和五款 3D 模型的统一提交都受理符合规则的 `callback_url`,该字段不进入上游请求体。通知来自平台已持久化的任务状态,使用平台任务 ID;其签名是 **mote 平台签名**,不是厂商原始签名。
## 注册与通知示例
先设置 `GATEWAY_ORIGIN` 与 `KEY`,方法见[快速开始](overview.md)。替换 `callback_url` 为你的接收服务:
```bash
curl -s "$GATEWAY_ORIGIN/media/v1/videos/generations" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: callback-video-001" \
-d '{
"model": "doubao-seedance-2-5-260628",
"prompt": "蓝色杯子,静止镜头",
"duration": 4,
"resolution": "480p",
"generate_audio": false,
"callback_url": "https://callbacks.example.com/mote",
"wait": 0
}'
```
提交响应仍是正常任务对象。平台向接收地址发送 `POST`,示例通知如下:
```http
Content-Type: application/json
X-Heighliner-Event-Id: 平台生成的事件标识
X-Heighliner-Event-Sequence: 2
X-Heighliner-Signature: t=1791993600,v1=64位小写十六进制签名
```
```json
{
"object": "media.task.event",
"id": "task_替换为平台任务ID",
"model": "doubao-seedance-2-5-260628",
"type": "video",
"status": "succeeded",
"request_id": "提交请求的关联标识"
}
```
平台协议通知只包含状态和查询标识;收到后按 `id` 查询产物、资金状态及安全错误。厂商兼容接口保留各自的已支持回调包装,详见对应原生文档。不会包含 Provider Key、采购价格或完整上游错误。
## Python 验签
接收服务应先读**未经重新编码的 HTTP 正文 bytes**。不要先解析 JSON 再序列化,否则空格、键顺序或 Unicode 编码变化都会导致验签失败。
```python
import hashlib
import hmac
import time
def verify_callback(headers, raw_body: bytes, virtual_key: str):
fields = dict(part.split("=", 1) for part in
headers["X-Heighliner-Signature"].split(","))
timestamp = fields["t"]
event_id = headers["X-Heighliner-Event-Id"]
sequence = headers["X-Heighliner-Event-Sequence"]
if not event_id or not sequence.isdecimal():
raise ValueError("invalid event headers")
if abs(time.time() - int(timestamp)) > 300:
raise ValueError("expired callback; check server clock")
# 与请求鉴权使用同一虚拟密钥:去 sk- 前缀及固定渠道后缀。
core = virtual_key.removeprefix("sk-").split("-", 1)[0]
signed = f"{event_id}.{sequence}.{timestamp}.".encode() + raw_body
expected = hmac.new(core.encode(), signed, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, fields["v1"]):
raise ValueError("invalid callback signature")
return event_id, int(sequence)
```
验签后,把事件 ID 写入带唯一约束的持久去重表,并将业务处理放入你的队列;已接收的重复事件直接返回 2xx。以任务 ID 为单位比较序号,避免迟到事件覆盖较新结果。不要记录虚拟密钥。接收服务应在 3 秒内返回 2xx;非 2xx、超时和网络中断会触发重试,重定向不会被跟随。
## H3 的 challenge 验证
H3 在注册回调时会先收到独立验证请求,例如 `{"challenge":"随机字符串"}`。接收服务须在 3 秒内以 JSON 原样回显同一个 `challenge`。验证请求没有任务事件头,不应触发业务状态更新,也不能被当成已验签任务通知。验证通过后才允许提交生成;后续状态通知仍按上面的平台 HMAC 验签。
## 时序、重试与失败处理
- 通知基于平台轮询观测到的状态,可能晚于厂商状态变化;平台不会补造两次轮询之间未观察到的中间状态。任务查询始终是确认结果与账务状态的依据,回调不能替代它。
- 同一任务按事件序号依次尝试投递。失败后间隔约为 1、5、15、60、300、900、3600 秒,最多尝试 8 次;实际发送还受工作者调度影响。
- 重试保持事件 ID、序号和正文不变,时间戳及签名随本次发送更新。投递可能重复,无法承诺网络上恰好一次;达到上限后不继续自动投递。
- 回调失败不会重新提交生成,也不改变已确认账单。不能因为没有收到回调就重新生成。
- 当前虚拟密钥、消费身份或模型权限被撤销后停止投递。额度用尽但身份仍有效的既有任务可以继续通知与查询。
- 旧任务未登记回调时不会补发历史通知。提交结果未知时保留原平台任务和资金,查询核对,平台不伪造成功事件。