在控制台中设置 Webhook,接收投递并验证 HMAC 签名,附有 curl、Node.js 和 PHP 的示例代码。
Webhook 会在你的工作区发生某件事时(例如页面被发布)立即向你的服务器发送一个 HTTP 请求。这样你就不必反复查询是否有变化。每次投递都带有签名,你的服务器可以据此区分真实消息和伪造消息。
前提条件
- 工作区的所有者或管理员;只有这两种角色才会看到“Webhook”菜单项
- Creator Pro 起提供;在 Creator Free 中显示“Webhook 自 Creator Pro 起提供。”
- Creator Pro 最多 5 个 Webhook,Creator Agency 最多 25 个,Enterprise 最多 100 个(可调整)
- 一台可以通过公开 https 地址访问的服务器
Webhook 只能在控制台中管理,无法通过 API 密钥创建或修改。
创建 Webhook
- 在导航中打开“Webhook”
- 点击“+ 新建 Webhook”
- 输入目标网址并选择事件
- 可选:输入描述并选择状态
- 点击“创建 Webhook”
- 在“完成”步骤中复制签名密钥(“签名密钥——仅此一次可见”)并保存到你的服务器上,它只会在此时显示
- 用“测试”测试 Webhook。结果会一直显示在卡片上,直到下一次测试:“投递成功”或“未投递”,并附带 HTTP 状态码(或“无响应”)、耗时和响应的开头部分
- “目标网址(https://…)”
- 最多 2048 个字符。仅限 https,网址中不能包含凭据,不能是 localhost 地址,也不能是 .local、.internal 或 .lan 等内部名称。该名称必须指向公网 IP 地址;LinkHarbor 在每次投递前都会再次检查。
- “事件”
- 至少选择一个事件。每组都有“全选”和“取消全选”。
- “描述”
- 可选,最多 255 个字符,例如写明这个 Webhook 的用途。
- “状态”
- “已启用”或“已停用”。已停用的 Webhook 不会收到投递。
事件
| 事件 | 在控制台中 | 触发时机 | data 的内容 |
|---|---|---|---|
| page.published | 页面已发布 | 页面被发布,无论是在控制台中、通过 API 还是由 AI 助手发布 | page_id、version、checksum、published_by_user_id(API 密钥和 AI 助手发布时为 null) |
| form.submitted | 表单已提交 | 访客在你的某个页面上提交了表单 | form_id、form_name、submission_id、page_id、block_id、submitted_at、values(字段键 → 值)、consent(同意项键 → true/false) |
| domain.verified | 域名已验证 | 自定义域名验证成功 | domain_id、hostname、verification_method、status |
| domain.disabled | 域名已停用 | 自定义域名在控制台中被停用(降级时不会触发) | domain_id、hostname、status |
| webhook.test | –(“测试”按钮) | 你点击“测试”时 | message(使用发起测试者的账户语言的提示文本,不要据此匹配) |
form.submitted 包含访客提交的原始值,但绝不包含访客的 IP 地址或浏览器信息。投递在后台进行:访客无需等待,投递失败也不会影响提交。
投递的结构
LinkHarbor 以 POST 请求发送每次投递,带有 JSON 请求体和以下请求头:
| 请求头 | 内容 |
|---|---|
| Content-Type | application/json |
| X-LinkHarbor-Event | 事件名称,例如 page.published |
| X-LinkHarbor-Delivery-Id | 本次投递的唯一 ID(UUID),与请求体中的 id 相同 |
| X-LinkHarbor-Timestamp | 发送时间,以 Unix 秒表示 |
| X-LinkHarbor-Signature | sha256= 后接十六进制的 HMAC-SHA256 签名 |
| X-LinkHarbor-Signature-Version | v1 |
{
"id": "3f1c2a9e-8d4b-4a51-9f0e-6b7c1d2e3f40",
"event": "page.published",
"created_at": "2026-11-01T09:30:12+00:00",
"data": {
"page_id": 42,
"version": 8,
"checksum": "9b1e5c…",
"published_by_user_id": 7
}
} 重新投递会获得新的 id,另外还有 redelivery_of(被重复的那次投递的 ID)和 delivery_root(这条链中第一次投递的 ID)两个字段。
验证签名
每个 Webhook 都有自己的签名密钥(32 个十六进制字符)。你只能看到它一次:在创建 Webhook 的最后一步,以及点击“轮换密钥 ↺”之后。请立即把它复制到你的服务器上,LinkHarbor 以后不会再显示它。LinkHarbor 用它按如下方式计算签名:
- 构建消息:时间戳、点、投递 ID、点、原始请求体,即 timestamp.delivery_id.body
- 以签名密钥为密钥,对这条消息计算 HMAC-SHA256
- 把结果作为带 sha256= 前缀的十六进制字符串发送
你的服务器应使用恒定时间比较来检查签名,拒绝时间戳超过 300 秒的投递,并至少在这段时间内保存投递 ID,以识别重复投递。重要:签名针对的是原始请求体,即到达时的原样内容,而不是你重新编码后的 JSON。
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.LH_WEBHOOK_SECRET;
const seen = new Set(); // 生产环境中:使用带过期时间的数据库或 Redis
app.post('/webhooks/linkharbor', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('X-LinkHarbor-Timestamp') || '';
const id = req.get('X-LinkHarbor-Delivery-Id') || '';
const sig = req.get('X-LinkHarbor-Signature') || '';
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
return res.status(400).send('timestamp');
}
const expected = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(ts + '.' + id + '.')
.update(req.body) // 原始请求体(Buffer)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(sig);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('signature');
}
const event = JSON.parse(req.body.toString('utf8'));
const key = event.delivery_root || event.id; // 识别同一条链中的重新投递
if (!seen.has(key)) {
seen.add(key);
// 在这里处理,最好放进队列异步处理
}
res.sendStatus(200);
});
app.listen(3000); <?php
$secret = getenv('LH_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_LINKHARBOR_TIMESTAMP'] ?? '';
$id = $_SERVER['HTTP_X_LINKHARBOR_DELIVERY_ID'] ?? '';
$sig = $_SERVER['HTTP_X_LINKHARBOR_SIGNATURE'] ?? '';
if (!ctype_digit($ts) || abs(time() - (int)$ts) > 300) {
http_response_code(400);
exit('timestamp');
}
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $id . '.' . $body, $secret);
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit('signature');
}
$event = json_decode($body, true);
// 通过 $event['delivery_root'] ?? $event['id'] 识别重复投递,
// 然后再处理。
http_response_code(200); 用 curl 测试你的端点
这个脚本会向你自己的服务器发送一个正确签名的测试投递,在 LinkHarbor 中创建 Webhook 之前就能派上用场:
SECRET="your-signing-secret"
URL="https://example.com/webhooks/linkharbor"
TS=$(date +%s)
ID=$(uuidgen | tr 'A-Z' 'a-z')
BODY='{"id":"'"$ID"'","event":"webhook.test","created_at":"2026-11-01T09:30:12+00:00","data":{"message":"这是该 Webhook 的测试事件。"}}'
SIG="sha256=$(printf '%s' "$TS.$ID.$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')"
curl -X POST "$URL" \
-H "Content-Type: application/json" \
-H "X-LinkHarbor-Event: webhook.test" \
-H "X-LinkHarbor-Delivery-Id: $ID" \
-H "X-LinkHarbor-Timestamp: $TS" \
-H "X-LinkHarbor-Signature: $SIG" \
-H "X-LinkHarbor-Signature-Version: v1" \
-d "$BODY" 响应、超时和重新投递
- 任何 2xx 状态的响应都算成功;不会跟随重定向(3xx)
- 你的服务器有 10 秒时间响应,建立连接最多 5 秒
- 同一事件的所有 Webhook 总共共享 25 秒;到时还没轮到的会被标记为失败,之后可以重新投递
- LinkHarbor 不会自动重试失败的投递
- 你可以手动重新发送失败的投递,但之后已经成功的投递不能重发。重新投递会有新的 id 和新的 created_at
- 每个事件最多允许 3 次尝试(包括第一次投递);之后它会进入“死信”,无法再重新发送
控制台中的投递记录
“投递记录”会打开 Webhook 的日志,其中有三个标签页:
- “所有投递”
- 所有投递,可按事件、状态(“成功(2xx)”“客户端错误(4xx)”“服务器错误(5xx)”“失败”)和“数量上限”筛选。只要之后没有成功的投递,“↺ 重试”就会重新投递失败的那一次。点击某一行可以查看请求、响应、请求体和原始 JSON。
- “重放”
- 重新投递的链条,带有结果“已恢复”或“仍在失败”以及尝试次数。
- “死信”
- 所有允许的尝试都失败的投递,包括第一次投递和各次重新投递。这个标签页只能查看:其中的投递无法再重新投递。请检查你的端点;之后新的事件会照常投递。如果你需要这些数据,请通过 API 获取。
如果重新投递没有成功,控制台会给出原因:
- “未重新投递:此事件已失败 … 次,无法再尝试。你可以在‘死信’中找到它。”
- 3 次尝试都已用完。
- “未重新投递:此事件此后已成功投递。”
- 同一条链中之后的某次投递已经成功。
控制台中的验证
在“验证”中有两个辅助工具:
- “验证工具包”
- “加载工具包”会显示算法、请求头名称、签名消息的格式以及推荐的 300 秒容差。其中不包含密钥本身。
- “验证签名”
- 输入你收到的某次投递的投递 ID、时间戳、签名和原始请求体。LinkHarbor 随后会告诉你“签名有效”或“签名无效”。时间戳和签名请从 X-LinkHarbor-Timestamp 和 X-LinkHarbor-Signature 请求头中复制。这项检查只适用于最近 5 分钟内的投递;对于更早的投递,即使签名正确,它也会报告“签名无效”。
轮换密钥、停用、删除
- “轮换密钥 ↺”会生成新的签名密钥并显示一次;旧密钥立即失效,你的服务器必须改用新密钥
- 在“编辑”中把状态设为“已停用”,即可暂停投递
- 垃圾桶图标会删除 Webhook;在你关闭提示消息之前,可以用“撤销”取消删除
降级到更小的套餐时,超出新上限的 Webhook 会被停用,降级到 Creator Free 时则全部停用。它们会被保留;升级后,LinkHarbor 会自动重新启用已停用的 Webhook,直到达到新套餐的上限,其中也包括你自己停用的 Webhook。
你可能还想了解
没找到答案?
告诉我们你需要什么,我们的客服团队会亲自帮你。