> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cyberun.cloud/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> 在任务状态变化时接收 HTTP 回调。

Webhook 将事件投递到你控制的 HTTPS 端点。可用于在不轮询的情况下
对任务完成或失败作出反应。

## 开始之前

* 你的侧边栏需要有 **Webhooks** 入口。如果没有，
  说明此部署未启用 webhook —— 请联系你的团队管理员。
* 你需要在拥有该 webhook 的团队中具备管理员角色。
* 一个可从公网访问的 HTTPS 端点。不接受 `http://`。

## 创建 webhook

1. 打开侧边栏中的 **Webhooks**。
2. 点击 **创建 webhook**。
3. 填写：
   * **URL** —— 你的端点。必须为 `https://`。
   * **事件** —— 选择哪些事件会触发投递。见下方列表。
   * **描述** —— 内部标签。
4. 点击 **创建**。控制台会显示 webhook 的 **签名密钥**。
   立即复制 —— 与凭证一样，只显示一次。

## 事件

目前投递两个任务事件。

| 事件               | 触发时机                     |
| ---------------- | ------------------------ |
| `task.completed` | 任务干净地完成。负载包含任务输出。        |
| `task.failed`    | 任务出错、超时或其代理已断开。负载包含错误信息。 |

更细粒度的状态转换（queued、running、cancelled）请改为订阅任务的
[SSE 事件流](/zh/api-reference/streaming-events)，而不是 webhook。

## 负载结构

每次投递都是一个 JSON `POST`。请求头：

```http theme={null}
POST /your/path HTTP/1.1
Content-Type: application/json
X-Webhook-Signature-256: sha256=<hex_hmac>
```

主体：

```json theme={null}
{
  "event_type": "task.completed",
  "task_id": "019c65a0-aaaa-7bbb-cccc-dddddddddddd",
  "task_status": "completed",
  "task_output": {
    "storage_key": "tasks/019c65a0.../output.png",
    "image_count": 1
  },
  "task_error": "",
  "timestamp": "2026-02-16T09:00:00Z"
}
```

`task.failed` 投递的结构相同，`task_status` 为 `"failed"`、
`task_output` 为 `null`，并由 `task_error` 给出失败原因。

| 字段            | 含义                                   |
| ------------- | ------------------------------------ |
| `event_type`  | `task.completed` 或 `task.failed`。    |
| `task_id`     | 触发事件的任务 UUID。                        |
| `task_status` | 终态任务状态：`completed` 或 `failed`。       |
| `task_output` | 成功时为任务输出对象（结构按工作流运行时而定），失败时为 `null`。 |
| `task_error`  | 失败时为错误字符串，成功时为空字符串。                  |
| `timestamp`   | 事件触发时间，ISO 8601 格式。                  |

## 验证签名

Cyberun 用创建 webhook 时显示的密钥，以 HMAC-SHA256 对每次投递签名。
**在信任负载之前请先验证签名。**

签名在原始请求体上计算，通过
`X-Webhook-Signature-256: sha256=<hex_hmac>` 请求头发送。

```ts theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(body: Buffer, header: string, secret: string): boolean {
  const expected = createHmac('sha256', secret).update(body).digest('hex');
  const received = header.replace(/^sha256=/, '');
  return (
    expected.length === received.length &&
    timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(received, 'hex'))
  );
}
```

```python theme={null}
import hmac, hashlib

def verify(body: bytes, header: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", header)
```

对缺失或不匹配签名的投递，返回 HTTP `401` 拒绝。

## 及时回执

* 尽快回复 `2xx`。响应较慢会算作失败并触发重试策略。
* 把繁重的工作异步执行。处理器应当校验签名、把任务入队后立即返回 ——
  不要在下游系统上阻塞。

## 重试

在 `5xx` 或超时时，Cyberun 最多重试 **4 次**，采用指数退避：
即时 → 5 秒 → 30 秒 → 2 分钟。全部重试失败后会丢弃这次投递。

`task_id` 在所有重试中保持不变 —— 你的处理器应该把同一组
`task_id` + `event_type` 的重复投递视为无操作。

## 轮换签名密钥

如果怀疑密钥泄露，打开 webhook 的详情页并轮换。新密钥只显示一次，
之后所有投递都立即用新密钥签名。

## 不要

* ❌ 在没有验证签名的情况下信任负载。
* ❌ 在请求处理器中阻塞执行慢任务 —— 先回执，后处理。
* ❌ 把签名密钥暴露在客户端代码中。它是服务端密钥。

## 相关

* [任务](/zh/cloud/concepts/tasks) —— 你最常会响应的事件。
* [订阅任务事件流（SSE）](/zh/api-reference/streaming-events) ——
  逐步进度，而不只是终态事件。
* [API 参考](/zh/api-reference/introduction) —— webhook 管理端点。
