跳到正文
RunAPI 开发者文档
指南
指南

回调

安全接收并验证 Task 回调投递。

通过回调将 Task 生命周期事件发送到公网 HTTPS 端点。处理 JSON body 前先验证每次回调,确保只有为当前账户签名的投递能够改变应用状态。

配置 callback URL

创建 Task 时加入公网 HTTPS callback_url。事件 body 与生命周期状态取决于具体端点,请从对应端点的 API Reference 查看它声明的 callback payload。

JSON
{
  "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,按下面的方式精确构造待签名内容:

TEXT
X-Callback-Id + "." + X-Callback-Timestamp + "." + raw request body

先以 Base64 解码 Callback Secret,再使用它对上述内容计算 HMAC-SHA-256,随后将结果 Base64 编码,并与 X-Callback-Signature 做恒定时间比较。不要在校验前解析 JSON 再重新序列化。

JavaScript

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

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 快速开始