---
title: 回调
url: https://runapi.ai/zh-CN/docs/guides/task-api/callbacks.md
canonical: https://runapi.ai/zh-CN/docs/guides/task-api/callbacks
locale: zh-CN
---

# 回调

通过回调将 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](/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
快速开始](/zh-CN/docs/guides/task-api/quickstart)。
