# 任务回调 > 在任务状态变化时接收通知,校验平台签名,安全去重,再按平台任务 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、序号和正文不变,时间戳及签名随本次发送更新。投递可能重复,无法承诺网络上恰好一次;达到上限后不继续自动投递。 - 回调失败不会重新提交生成,也不改变已确认账单。不能因为没有收到回调就重新生成。 - 当前虚拟密钥、消费身份或模型权限被撤销后停止投递。额度用尽但身份仍有效的既有任务可以继续通知与查询。 - 旧任务未登记回调时不会补发历史通知。提交结果未知时保留原平台任务和资金,查询核对,平台不伪造成功事件。