跳转到内容

Webhook 协议参考

这是 Webhook 模式的完整协议参考。如果你只想跑通 webhook 部署,先看 Hello World — Webhook 模式;本页是 深入字段定义、签名验证细节、重试策略、错误恢复。

技术真源:Webhook 投递规范 v1.0。

注册 Webhook URL

通过 System Agent Hashee:

把 DemoBot 的连接模式改成 webhook
URL: https://my-agent.example.workers.dev/hashee/webhook
secret: <随机 base64 字符串,至少 32 字节>

System Agent 会显示一张 Artifact 表单二次确认;提交后立即生效,旧 WS 连接 (如有)会被关闭。

Webhook 请求 wire format

每条 incoming POST:

POST https://my-agent.example.workers.dev/hashee/webhook HTTP/1.1
Content-Type: application/json
Content-Length: 1024
X-Hashee-Signature: abcdef0123... (hex, 64 chars)
X-Hashee-Timestamp: 1731200000 (unix epoch seconds)
X-Hashee-Delivery-Id: 01HZX9... (UUID v7)
X-Hashee-Agent-Id: 01906abc-...
{
"type": "message.new",
"version": "v1",
"wire_envelope": "<base64 hashee-envelope-v1.x>",
"metadata": {
"conversation_id": "01HZ...",
"sender_id": "01HZ...",
"created_at": "2026-05-15T10:24:11.123Z"
}
}

事件类型(Webhook 子集)

Webhook 不传所有 WebSocket 事件,只传业务相关的:

event.type何时
message.new有新入站消息
relation.established用户加你 Agent
relation.revoked用户撤销关系
relation.suspended平台暂停(违规等)
relation.restored平台恢复
artifact_response用户回了之前发的 artifact
agent.capability_changed用户接受 / 拒绝你的 manifest 变更

通过 webhook 传的(仅 WS):

  • agent.governance — 治理状态
  • reaction.update — 消息反应
  • group.updated — 群信息
  • artifact.expired — TTL 过期
  • session.invalidated
  • auth.expiring — token 即将过期提醒
  • system.group_key_rotated

如果你需要这些事件,必须切回 WebSocket 模式。

HMAC 签名验证

签名算法:

signed_string = timestamp + "." + delivery_id + "." + body
signature = hex(HMAC_SHA256(webhook_secret, signed_string))

接收侧验证:

import { verifyWebhookSignature } from "@hasheeai/agent-sdk-ts";
const valid = await verifyWebhookSignature(
webhookSecret, // 你预先设的 base64 字符串
request.headers, // headers 对象
rawBodyText, // body 原始字符串
);
if (!valid) {
return new Response("invalid signature", { status: 401 });
}

Python

import hmac, hashlib, time
def verify(secret: str, headers: dict, body: str) -> bool:
ts = headers.get("X-Hashee-Timestamp") or headers.get("x-hashee-timestamp")
sig = headers.get("X-Hashee-Signature") or headers.get("x-hashee-signature")
delivery_id = headers.get("X-Hashee-Delivery-Id") or headers.get("x-hashee-delivery-id")
if not (ts and sig and delivery_id): return False
if abs(int(time.time()) - int(ts)) > 300: return False
signed = f"{ts}.{delivery_id}.{body}"
expected = hmac.new(secret.encode(), signed.encode(), hashlib.sha256).hexdigest()
return hmac.compare_digest(sig, expected)

Go

import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"strconv"
"time"
)
func verify(secret string, ts, deliveryID, sig string, body []byte) bool {
tsInt, err := strconv.ParseInt(ts, 10, 64)
if err != nil { return false }
if abs(time.Now().Unix()-tsInt) > 300 { return false }
signed := ts + "." + deliveryID + "." + string(body)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signed))
expected := hex.EncodeToString(mac.Sum(nil))
sigB, e1 := hex.DecodeString(sig)
expB, e2 := hex.DecodeString(expected)
if e1 != nil || e2 != nil { return false }
return hmac.Equal(sigB, expB)
}

PHP

function verify(string $secret, array $headers, string $body): bool {
$ts = $headers['X-Hashee-Timestamp'] ?? null;
$sig = $headers['X-Hashee-Signature'] ?? null;
$deliveryId = $headers['X-Hashee-Delivery-Id'] ?? null;
if (!$ts || !$sig || !$deliveryId) return false;
if (abs(time() - (int)$ts) > 300) return false;
$signed = $ts . '.' . $deliveryId . '.' . $body;
$expected = hash_hmac('sha256', $signed, $secret);
return hash_equals($sig, $expected);
}

Rust

use hmac::{Hmac, Mac};
use sha2::Sha256;
use subtle::ConstantTimeEq;
type HmacSha256 = Hmac<Sha256>;
fn verify(secret: &str, ts: &str, delivery_id: &str, sig_hex: &str, body: &[u8]) -> bool {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64;
let ts_int: i64 = ts.parse().unwrap_or(0);
if (now - ts_int).abs() > 300 { return false; }
let signed = format!("{}.{}.", ts, delivery_id);
let mut mac = HmacSha256::new_from_slice(secret.as_bytes()).unwrap();
mac.update(signed.as_bytes());
mac.update(body);
let expected = hex::encode(mac.finalize().into_bytes());
sig_hex.as_bytes().ct_eq(expected.as_bytes()).into()
}

Cargo: hmac = "0.12", sha2 = "0.10", hex = "0.4", subtle = "2.5".

cURL 测试(生成签名 dry-run)

Terminal window
SECRET="your-webhook-secret"
TS=$(date +%s)
ID=$(uuidgen)
BODY='{"event":"test","data":{}}'
SIGNED="${TS}.${ID}.${BODY}"
SIG=$(printf '%s' "$SIGNED" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
curl -X POST https://my-agent.example.com/hashee/webhook \
-H "Content-Type: application/json" \
-H "X-Hashee-Timestamp: $TS" \
-H "X-Hashee-Delivery-Id: $ID" \
-H "X-Hashee-Signature: $SIG" \
-d "$BODY"

便于本地 dry-run 你自己的 webhook handler,无需 SDK。

verifyWebhookSignature 内部做 4 件事:

  1. 读取 X-Hashee-Timestamp → 检查 |now - ts| ≤ 300s,否则拒(防重放)
  2. 读取 X-Hashee-Signature → 用 webhook_secret 重算 → 常时间比较
  3. 读取 X-Hashee-Delivery-Id → 在过去 24h 内见过 → 直接 200 OK 跳过(幂等)
  4. 解析 body 验证 envelope 格式

verifyWebhookSignature 不解密,仅签名 + 时间戳 + 去重检查。 解密走 parseWebhookPayload(或者 createWebhookDispatcher 帮你做)。

完整 Dispatcher 示例

import { createWebhookDispatcher } from "@hasheeai/agent-sdk-ts";
const dispatcher = createWebhookDispatcher({
agentId: env.HASHEE_AGENT_ID,
token: env.HASHEE_AGENT_TOKEN,
secret: env.HASHEE_WEBHOOK_SECRET,
privateKeyBase64: env.HASHEE_X25519_PRIVATE_BASE64,
signingPrivateKeyBase64: env.HASHEE_ED25519_PRIVATE_BASE64,
baseUrl: "https://api.hashee.ai",
onMessage: async (msg) => {
// 已自动解密 + 验签 + 解析
console.log(`message from ${msg.sender_id}: ${msg.payload?.text}`);
},
onEvent: async (event) => {
if (event.type === "relation.revoked") {
await cleanupUser(event.payload.user_id);
}
},
// 可选
autoRecover: true, // 默认 true:init 时自动尝试 recover unreachable
timestampToleranceSeconds: 300, // 默认 300,spec 上限 300
deliveryIdDedupeWindowMs: 86400000, // 默认 24h
});
// 在你的 HTTP handler 里
await dispatcher.handle(headers, body);

R2 引用 — 大 payload

单 webhook body 硬上限 1 MB。超过时后端把密文上传到 R2 并发送一个 stub envelope:

{
"type": "message.new",
"version": "v1",
"wire_envelope": null,
"ref": {
"type": "r2",
"object_key": "ref://abc123",
"size": 5242880,
"content_type": "application/octet-stream"
},
"metadata": { ... }
}

SDK Dispatcher 自动检测 ref.type === "r2" → GET /r2/<object_key> 拿密文 → 解密 → 还原成完整 InboundMessage 喂给你的 handler。对你完全透明

R2 fetch 也走 Webhook agent 的 token 鉴权;速率:5 req/s per Agent。

重试策略

后端发送 webhook 后等待你的 HTTP 响应:

响应后端行为
2xx投递成功
429 + Retry-After: <s>按 Retry-After 重试
4xx (非 429)永久失败,不重试(如签名错、参数错)
5xx 或超时临时失败,触发重试
超过 10 秒未响应视为超时

重试节奏(指数退避):

失败 1: 立即重试
失败 2: 30s 后
失败 3: 2 min 后
失败 4: 8 min 后
失败 5: 32 min 后
失败 6: 2 h 后
失败 7: 6 h 后
失败 8: 标 unreachable,停止重试

重试期间消息保留在后端 pending 队列;标 unreachable 后队列继续接受新消息 (最多 24 小时),等你 recover。

Unreachable 状态机

你的 webhook 7 次重试都失败
后端:agents.connection_status = "unreachable"
新 inbound 消息进入 pending 队列(保留 24 小时)
> 24 小时后 pending 队列清空(消息丢失,发送方收到投递失败提示)
你修复 webhook
方式 A: SDK Dispatcher 下次启动自动 recover(autoRecover: true)
方式 B: 主动 POST /agents/:id/connection/recover
后端:unreachable → ready,flush pending 队列

主动 recover:

import { restRequest } from "@hasheeai/agent-sdk-ts";
const result = await restRequest({
method: "POST",
baseUrl: "https://api.hashee.ai",
path: `/agents/${AGENT_ID}/connection/recover`,
token: AGENT_TOKEN,
});
console.log(`pending: ${result.pending_count}`);

Delivery log

后端为每个 webhook 投递保留 30 天日志:

字段类型
delivery_iduuid v7
agent_iduuid
event_typestring
created_at, completed_attimestamptz
attempt_countinteger
final_status”delivered” / “failed” / “unreachable”
latency_msinteger (delivered 时)
error_classstring (failed 时)

查询:

Terminal window
curl -X GET "https://api.hashee.ai/agents/<agent_id>/delivery-logs?limit=50" \
-H "Authorization: Bearer hsk_..."

返回数组按时间倒序。

主动推送

Webhook agent 没长连接,需要 REST:

import { restRequest } from "@hasheeai/agent-sdk-ts";
await restRequest({
method: "POST",
baseUrl: "https://api.hashee.ai",
path: `/agents/${AGENT_ID}/messages`,
token: AGENT_TOKEN,
body: {
conversation_id: targetConversationId,
payload: { type: "text", text: "提醒:你的部署完成了" },
},
});

或者(封装版):

const dispatcher = createWebhookDispatcher({ /* ... */ });
await dispatcher.sendReply(conversationId, payload);

服务器侧不要做

  • 不要把 webhook handler 同步等长任务(>10s)。立即 200 ack + 异步处理。
  • 不要忽略 X-Hashee-Delivery-Id 去重 → 否则消息会被处理多次。
  • 不要在 handler 里抛异常但又返 200 → 异常会丢消息但后端以为成功。
  • 不要信任未验签的 body → MITM / fake webhook 攻击。

错误码

你回后端理解
200 / 201 / 204OK
429 + Retry-After限速,按 header 等
400Bad Request 永久失败(签名错 / 参数错)
401 / 403鉴权失败永久失败(多半是签名错)
404 / 410URL 错永久失败
500-599 / 超时临时失败,重试

下一步