WEBHOOKS · 免轮询
Webhook 回调:任务完成自动推送
视频生成需要几十秒到几分钟。与其轮询 GET /v1/videos/{id},
配置一个回调 URL:任务到达终态(成功或失败)时,我们主动 POST 一份结构化事件给你的服务端,
带 HMAC-SHA256 签名。轮询与回调可同时使用,互不冲突。
1. 配置接收端
登录控制台 → 个人设置 → 通知方式:选择 Webhook,填入 Webhook 地址(你的接收端 URL,须为公网可达的 HTTPS 地址)和 Webhook 密钥(自定义字符串,用于验签,妥善保管)。保存后,你的每一个任务终态都会推送到这个地址。
2. 事件载荷
两个事件:task.succeeded(成功)与 task.failed(失败)。POST 请求体为 JSON:
task.succeeded
{
"event": "task.succeeded",
"task_id": "vid_7f3a9c",
"model": "kling-3.0",
"status": "SUCCESS",
"outputs": [
"https://cdn.modapis.com/.../out.mp4"
],
"quota": 42000,
"credits_consumed": 8.4,
"submitted_at": 1760851200,
"finished_at": 1760851268,
"timestamp": 1760851268
} task.failed
{
"event": "task.failed",
"task_id": "vid_8c1b22",
"model": "veo-3.1",
"status": "FAILURE",
"fail_reason": "upstream capacity exhausted",
"quota": 0,
"submitted_at": 1760851300,
"finished_at": 1760851312,
"timestamp": 1760851312
} 字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
| event | string | task.succeeded / task.failed |
| task_id | string | 任务 ID(提交接口返回的 id,同轮询接口) |
| model | string | 模型名,如 kling-3.0 |
| status | string | SUCCESS / FAILURE |
| outputs | string[] | 产物 URL 列表;失败事件无此字段。注意:URL 有时效,请及时转存 |
| quota | int | 本任务实扣 quota(500000 quota = $1;失败=0) |
| credits_consumed | float? | 上游计费单位(对账用),可能缺失 |
| fail_reason | string | 失败原因;成功事件无此字段 |
| submitted_at / finished_at | int | Unix 时间戳(秒) |
| timestamp | int | 本事件发出时间(秒),可用于新鲜度校验 |
3. 验证签名(务必做)
每次请求带 X-Webhook-Signature 头:以你配置的密钥为 key、
原始请求体字节为消息的 HMAC-SHA256 hex 值(64 个十六进制字符)。另有
X-Webhook-Event(事件名)与 X-ModAPIS-Timestamp(RFC3339 发出时间)两个头便于路由与重放防护。
注意:务必用原始 body 验签,先 parse 再 stringify 可能因字段顺序/空白不同导致验签失败。
// Node.js:验证 X-Webhook-Signature(express.raw 拿原始字节验签)
import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/modapis-webhook", express.raw({ type: "application/json" }), (req, res) => {
const expected = crypto
.createHmac("sha256", process.env.MODAPIS_WEBHOOK_SECRET)
.update(req.body) // Buffer:未解析的原始请求体
.digest("hex");
if (expected !== req.header("X-Webhook-Signature")) {
return res.status(401).end();
}
const { event, task_id, outputs } = JSON.parse(req.body.toString());
if (event === "task.succeeded") saveResult(task_id, outputs);
res.status(200).end();
}); # Python(FastAPI):用原始 body 验签
import hmac, hashlib
@app.post("/modapis-webhook")
async def hook(request: Request):
raw = await request.body()
expected = hmac.new(
os.environ["MODAPIS_WEBHOOK_SECRET"].encode(),
raw,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("X-Webhook-Signature", "")):
raise HTTPException(status_code=401)
data = await request.json()
if data["event"] == "task.succeeded":
save_result(data["task_id"], data["outputs"])
return {"ok": True} 4. 投递语义
- 响应约定:接收端返回 2xx 视为成功;其他状态码或超时(5s)视为失败。
- 重试:失败后最多重试 2 次(共 3 次,间隔约 1s/2s)。三次失败后放弃,该事件不会无限重投——建议关键业务同时保留轮询兜底。
- 顺序:同一任务只会收到一个终态事件,不保证跨任务的全局顺序。
- 幂等:极端情况下(如接收端先返回 5xx 后又处理成功)可能收到重复事件,请按
task_id幂等处理。 - 安全:接收端应校验签名、限制来源、不要在 URL 中携带凭据。
5. 自测
配置完成后,在已登录的控制台页面打开浏览器开发者工具(Console),执行:
fetch("/api/user/mod/webhook/test", { method: "POST", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: "{\\"event\\":\\"task.succeeded\\"}" }).then(r => r.json()).then(console.log)
会向你的接收端发送一条 task_id 为 webhook-test-0000 的合成事件,
返回值里带 status_code(200=送达)与错误信息,方便联调期快速排障。