设置智能体事件的 Webhook
当智能体任务完成或失败、智能体等待审批或文档站点发布时,在你的 https 端点上获取已签名的 POST 请求。涵盖事件、有效载荷、签名验证、重试和自动禁用。
Webhook 会在发生特定情况(例如智能体任务完成或智能体等待审批)时,立即向你的端点发送一个已签名的 POST,因此你的系统无需进行轮询。本文介绍了如何添加端点、相关事件、有效载荷、验证签名以及投递失败时的处理方式。
开始之前
- Webhook 适用于 Pro、Business 和 Custom 计划(“Webhook 适用于 Pro 及以上计划”)。
- 只有工作区所有者和管理员可以管理 Webhook。 其他成员会看到“只有所有者和管理员可以管理 Webhook”。
- 你需要一个公共的 https 地址,该地址需在 10 秒内返回
2xx状态码。 - 每个工作区最多可以拥有 10 个 Webhook。
添加 Webhook
打开 Webhook 选项卡
打开工作区菜单,选择 AI agents & API,然后打开 Webhooks。点击 新 Webhook。

每个 Webhook 都会显示其事件、上次发送时间以及密钥的末尾。 输入端点
- 端点 URL:“一个公共 HTTPS 地址。”例如
https://example.com/clevera/webhooks。 - 描述:可选,最多 200 个字符,例如“Slack 中继”。
- 端点 URL:“一个公共 HTTPS 地址。”例如
选择事件
勾选要接收的事件,或选择 所有事件,包括新事件。标记为 即将推出 的事件虽然可以被接受,但“目前还没有发送此事件的内容”。

标记为“即将推出”的事件虽然可以被接受,但目前尚未发送。 创建并保存密钥
点击 创建 Webhook。签名密钥(以
whsec_开头)仅显示一次:“立即复制:你将无法再次看到它。使用它来检查 Clevera-Signature 标头。”像保存密码一样保存它,然后点击 我已保存。发送测试事件
打开 Webhook 的 ⋯ 菜单并选择 发送测试事件。Clevera 会发送一次已签名的
ping事件(不重试)。“测试事件已发送”表示它已离开 Clevera;请检查 投递 以查看你端点的响应。
事件
| 事件 | 发送时间 |
|---|---|
job.started |
智能体启动的任务开始运行时。 |
job.needs_input |
任务等待人工处理时(例如审批)。包含链接和请求过期时间。 |
job.succeeded |
任务完成时。 |
job.failed |
任务失败时。包含错误代码。 |
job.canceled |
任务被取消,或其审批被拒绝或过期时。 |
approval.requested |
智能体在 智能体请求 中请求审批时。 |
approval.decided |
有人批准或拒绝了请求,或者请求过期时。 |
site.published |
文档站点发布时。 |
site.build_failed |
文档站点发布失败时。 |
ping |
你点击了 发送测试事件 时。无需订阅。 |
以下事件被列为 即将推出 且尚未发送:job.progress、video.ready、article.ready、research.completed、insights.weekly。
任务事件涵盖智能体启动的任务(通过 API 令牌或已连接的应用),不包括用户在应用中启动的视频。
Clevera 发送的内容
每次投递都是一个带有 JSON 正文和以下标头的 POST:
| 标头 | 值 |
|---|---|
Content-Type |
application/json |
User-Agent |
Clevera-Webhooks/1.0 |
Clevera-Event |
事件名称,例如 job.succeeded |
Clevera-Delivery |
投递的唯一 ID。重新投递会重复使用相同的 ID,因此请使用它来忽略重复项。 |
Clevera-Signature |
t=<unix time>,v1=<signature> |
有效载荷携带 ID、链接、状态和数字,绝不包含内容本身。要读取已完成的视频或文章,请使用令牌调用 API。
{
"id": "6f1c2d3e-4b5a-4c6d-8e9f-0a1b2c3d4e5f",
"event": "job.succeeded",
"created_at": "2026-10-20T14:03:11Z",
"data": {
"job": {
"id": "job_8k2m4n6p8r0t2v4x",
"kind": "video.create",
"status": "succeeded",
"error_code": null,
"subject": { "type": "video", "id": "vid_example" },
"credits_spent": 64
},
"needs_input": null,
"workspace": { "id": "tm_example" }
}
}
{
"id": "0b9a8c7d-6e5f-4a3b-9c2d-1e0f9a8b7c6d",
"event": "approval.requested",
"created_at": "2026-10-20T14:01:02Z",
"data": {
"approval": {
"id": "apr_example",
"kind": "spend",
"status": "pending",
"credits_high": 80,
"expires_at": "2026-10-21T14:01:02Z",
"decided_at": null,
"url": "https://app.clevera.ai/agents?tab=requests&team=tm_example"
},
"job": { "id": "job_8k2m4n6p8r0t2v4x", "kind": "video.create" },
"workspace": { "id": "tm_example" }
}
}
验证签名
在信任每次投递之前请先进行检查。签名是 HMAC-SHA256,使用你的签名密钥对时间戳、一个点和原始请求正文进行加密:<t>.<body>。
- 从
Clevera-Signature标头中读取t和v1。 - 将
HMAC-SHA256(secret, t + "." + rawBody)计算为十六进制。 - 以恒定时间将其与
v1进行比较。 - 拒绝
t超过 5 分钟的投递,以防止捕获的请求被重放。
import crypto from 'node:crypto';
export function verifyClevera(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));
const t = Number(parts.t);
if (!parts.v1 || !Number.isInteger(t)) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return expected.length === parts.v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
import hashlib, hmac, time
def verify_clevera(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.strip().split("=", 1) for p in header.split(","))
t, v1 = parts.get("t", ""), parts.get("v1", "")
if not t.isdigit() or not v1 or abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
function verifyClevera(string $rawBody, string $header, string $secret, int $tolerance = 300): bool
{
parse_str(str_replace(',', '&', $header), $parts);
$t = $parts['t'] ?? '';
$v1 = $parts['v1'] ?? '';
if (! ctype_digit($t) || $v1 === '' || abs(time() - (int) $t) > $tolerance) {
return false;
}
return hash_equals(hash_hmac('sha256', $t.'.'.$rawBody, $secret), $v1);
}
请使用接收到的原始正文。先解析再重新编码 JSON 会改变字节,导致签名不匹配。
重试和失败
当你的端点以 2xx 状态响应时,投递即视为成功。任何其他情况都会导致尝试失败:
- 任何其他状态,包括重定向(不跟随
3xx); - 10 秒内无响应(连接超时为 5 秒);
- 地址无法再解析到公共互联网。
Clevera 会在 1 分钟、5 分钟、30 分钟、2 小时和 6 小时后重试失败的投递,共 5 次。你的端点响应正文不会被读取。
当连续 15 次投递尝试均失败时,Webhook 将被关闭:“已在连续 15 次投递失败后关闭。请修复端点,然后开启它。”所有者和管理员会收到“Webhook 到 host 已关闭”的通知:“其投递持续失败。请修复端点,然后重新开启它。”在关闭期间发生的事件不会被发送。测试事件不计入此限制。
管理 Webhook
点击 Webhook 上的 投递 以查看其日志,或打开其 ⋯ 菜单以获取其他选项:
| 选项 | 功能 |
|---|---|
| 编辑 | 更改 URL、事件或描述。 |
| 投递 | 最近 100 次投递及其状态(已投递、失败、发送中)、尝试次数和 HTTP 代码。重新投递可再次发送一次。投递记录保留 30 天。 |
| 发送测试事件 | 发送一个已签名的 ping。 |
| 重新生成密钥 | “投递将立即使用新密钥签名。请更新你的端点以进行检查。”没有重叠期,因此请在操作后立即更新你的端点。 |
| 关闭 / 开启 | 暂停和恢复投递。 |
| 删除 | “我们将停止向其发送事件并删除其投递日志。此操作无法撤销。” |
如果工作区不再使用 Pro 计划
Webhook 仍会列出,也可以进行编辑和删除,但不会发送任何内容。等待重试的投递将以 plan_locked 失败,并且 发送测试事件 和 重新投递 会显示“Webhook 不在你的计划内。”升级回 Pro 计划后将恢复发送;没有任何内容会被删除。
故障排除
| 消息 | 原因 | 解决方法 |
|---|---|---|
| “Webhook 不在你的计划内。” | 工作区未处于 Pro、Business 或 Custom 计划。 | 升级到 Pro。 |
| “请使用 https:// 地址。” | URL 以 http:// 开头。 |
通过 HTTPS 提供端点。 |
| “请使用主机名,而不是 IP 地址。” | URL 使用了 IP 地址。 | 使用域名。 |
| “此地址在公共互联网上不可达。” | 主机解析为私有或本地地址。 | 使用公共端点或中继。 |
| “请从地址中删除用户名和密码。” | URL 包含 user:password@。 |
删除它们;改用签名检查。 |
| “请输入有效的网址。” | URL 无效。 | 检查是否有拼写错误。 |
| “请至少选择一个事件。” | 未勾选事件。 | 至少勾选一个事件。 |
| “此工作区已有 10 个 Webhook。” | 达到工作区限制。 | 删除一个不使用的 Webhook。 |
| “请先重新开启 Webhook。” | 你尝试对已关闭的 Webhook 进行重新投递。 | 开启它,然后重新投递。 |
| “此投递仍在发送中。” | 投递仍在重试。 | 等待其完成。 |
| 投递因 HTTP 代码失败 | 你的端点返回了错误。 | 检查端点日志,确保其快速返回 2xx;在响应后再执行耗时工作。 |
| 签名从不匹配 | 正文在检查前被更改,或密钥已过期。 | 对原始正文进行验证;如果不确定,请重新生成密钥。 |