回调
安全接收并验证 Task 回调投递。
通过回调将 Task 生命周期事件发送到公网 HTTPS 端点。处理 JSON body 前先验证每次回调,确保只有为当前账户签名的投递能够改变应用状态。
配置 callback URL
创建 Task 时加入公网 HTTPS callback_url。事件 body 与生命周期状态取决于具体端点,请从对应端点的 API Reference 查看它声明的 callback payload。
{
"model": "flux-2-pro-text-to-image",
"prompt": "干净影棚背景中的产品照片",
"callback_url": "https://your-domain.com/webhooks/runapi"
}
创建 Callback Secret
在创建 Task 的账户中打开 API Keys,创建 Callback Secret。将它保存在密钥管理系统中,只提供给回调接收端。它不是 API Key,绝不能放入 Task 请求。
这个 secret 会签名该账户的 callback。轮换后,后续投递会使用新签名;请立即更新每个回调接收端,并且只在处理在途投递的短暂过渡期内同时保留新旧两个值。
校验回调签名
每个 callback 都是 Content-Type: application/json 的 HTTP POST。RunAPI 不会在这个请求中添加 Authorization 请求头。反序列化 body 前,先验证以下请求头:
X-Callback-Id
本次投递尝试的唯一标识。
X-Callback-Timestamp
签名时的 Unix 时间戳,单位为秒。
X-Callback-Signature
Base64 编码的 HMAC-SHA-256 签名。
使用未修改的 request body bytes,按下面的方式精确构造待签名内容:
X-Callback-Id + "." + X-Callback-Timestamp + "." + raw request body
先以 Base64 解码 Callback Secret,再使用它对上述内容计算 HMAC-SHA-256,随后将结果 Base64 编码,并与 X-Callback-Signature 做恒定时间比较。不要在校验前解析 JSON 再重新序列化。
JavaScript
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyCallback({headers, rawBody, callbackSecret}) {
const callbackId = headers["x-callback-id"];
const timestamp = Number(headers["x-callback-timestamp"]);
const signature = headers["x-callback-signature"];
if (!callbackId || !signature || !Number.isSafeInteger(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const signedContent = `${callbackId}.${timestamp}.${rawBody}`;
const expected = createHmac("sha256", Buffer.from(callbackSecret, "base64"))
.update(signedContent, "utf8")
.digest();
const received = Buffer.from(signature, "base64");
return expected.length === received.length && timingSafeEqual(expected, received);
}
将框架提供的原始 request-body string 传给 rawBody,不要对已解析的数据调用 JSON.stringify。
Python
import base64
import hashlib
import hmac
import time
def verify_callback(headers, raw_body, callback_secret):
callback_id = headers.get("X-Callback-Id")
timestamp = headers.get("X-Callback-Timestamp")
signature = headers.get("X-Callback-Signature")
if not callback_id or not timestamp or not signature:
return False
try:
timestamp = int(timestamp)
secret = base64.b64decode(callback_secret, validate=True)
received = base64.b64decode(signature, validate=True)
except (ValueError, TypeError):
return False
if abs(time.time() - timestamp) > 300:
return False
signed_content = f"{callback_id}.{timestamp}.{raw_body}".encode("utf-8")
expected = hmac.new(secret, signed_content, hashlib.sha256).digest()
return hmac.compare_digest(expected, received)
将 HTTP framework 接收到的原始 request-body string 作为 raw_body 传入。
安全处理投递
- 只有在 callback 已被接收并进入处理流程后才返回
2xx。发生非2xx响应或传输失败时,整次投递最多尝试 10 次。 - 在 15 秒内响应。校验通过后,将耗时工作放入队列,避免阻塞 HTTP 响应。
- 拒绝缺少签名请求头、签名无效或超出接收端定义时间窗口的 callback。
排查签名验证
- 签名不匹配: 确认 Callback Secret 与 Task 属于同一账户,先使用 Base64 解码 secret,并且签名原始 body 而不是解析后的 JSON。
- 缺少签名请求头: 在依赖 callback 改变应用状态前,先创建 Callback Secret。
- 时间戳被拒绝: 同步接收端的系统时钟,并使用适合部署环境的时间容忍窗口。
如需创建 Task 与了解生命周期,请返回Task API 快速开始。