FastComments.com

网络钩子

使用 FastComments,可以在每当评论被添加、更新或从我们的系统中删除时调用一个 API 端点。

我们通过基于 HTTP/HTTPS 的异步 webhooks 来实现这一功能。

什么是网络钩子 Internal Link

Webhook 是一种机制,或两套系统之间的集成,其中“生产者”(FastComments)触发一个事件 由“消费者”(您)通过 API 调用来消费该事件。

支持的事件和资源 Internal Link

FastComments 仅支持对 Comment 资源的 webhook。

我们支持对评论的创建、删除和更新的 webhook。

这些在我们的系统中被视为独立的事件,因此 webhook 事件具有不同的语义和结构。

任意数量的端点可以从仪表板或通过 API 订阅同一事件(参见通过 API 管理 Webhook)。每个 webhook 都会独立发送。

测试 Internal Link

新的和编辑 webhook 页面有一个 Send Test Payload 按钮,它会向表单中当前填写的 URL 发送请求,无论该 URL 是否已保存。Create 和 Update 事件会发送一个虚拟的 WebhookComment 对象,而测试 Delete 则会发送一个仅包含 ID 的虚拟请求体。

验证载荷

在测试你的 webhook 集成时,请验证传入的请求是否包含以下头部:

  1. X-FastComments-Timestamp - Unix 时间戳(秒)
  2. X-FastComments-Signature - HMAC-SHA256 签名

在签名方案引入之前创建的 webhook 还会收到一个包含你的 API Secret 的 token 头部。新的 webhook 则不会。

使用 HMAC 签名验证来确保载荷的真实性。

测试工具

在开发过程中,你可以使用 webhook.site 或 ngrok 等工具来检查传入的 webhook 载荷。

事件类型

  • Create 事件:在创建新评论时触发。
  • Update 事件:在编辑评论时触发。
  • Delete 事件:在删除评论时触发。

每个 webhook 绑定到一个事件和一种 HTTP 方法(POST、PUT 或 DELETE)。每个事件都会在请求体中包含完整的评论数据(载荷格式请参见数据结构)。

数据结构 Internal Link

通过 webhook 发送的唯一结构是 WebhookComment 对象,其 TypeScript 定义如下。

WebhookComment 对象结构

"Create" 事件结构

"create" 事件的请求体是一个 WebhookComment 对象。

"Update" 事件结构

"update" 事件的请求体是一个 WebhookComment 对象。

"Delete" 事件结构

"delete" 事件的请求体是一个 WebhookComment 对象。

2023年11月14日的变更
之前 "delete" 事件的请求体只包含评论 id。现在它包含删除时的完整评论。

请求体中始终包含每个键。当评论的某个字段没有值时,请求体中为 null (布尔值为 false,列表为 []),因此每次投递的数据结构始终一致。

WebhookComment 对象
Copy CopyRun External Link
1
2interface WebhookComment {
3 /** 评论的 id。 **/
4 id: string
5 /** 标识评论线程的 id 或 URL。已规范化。 **/
6 urlId: string
7 /** 指向评论所在位置的 URL。 **/
8 url: string | null
9 /** 发表评论的用户 id。如果是 SSO,则带有租户 id 前缀。 **/
10 userId: string | null
11 /** 发表评论的用户的邮箱。 **/
12 commenterEmail: string | null
13 /** 在评论组件中显示的用户名。使用 SSO 时,可以是 displayName。 **/
14 commenterName: string
15 /** 原始评论文本。 **/
16 comment: string
17 /** 解析后的评论文本。 **/
18 commentHTML: string
19 /** 评论的外部 id。 **/
20 externalId: string | null
21 /** 父评论的 id。 **/
22 parentId: string | null
23 /** 发表评论时的 UTC 日期。 **/
24 date: UTC_ISO_DateString
25 /** 投票的总 karma(赞 - 踩)。 **/
26 votes: number
27 votesUp: number
28 votesDown: number
29 /** 如果用户评论时已登录、或已验证评论、或在评论时验证了会话,则为 true。 **/
30 verified: boolean
31 /** 评论被验证时的 UTC 日期。 **/
32 verifiedDate: UTC_ISO_DateString | null
33 /** 版主是否已将该评论标记为已审核。 **/
34 reviewed: boolean
35 /** 头像的位置或 base64 编码。仅当 SSO 传入的值是 base64 时才会是 base64。 **/
36 avatarSrc: string | null
37 /** 评论是被手动还是自动标记为垃圾评论? **/
38 isSpam: boolean
39 /** 评论是否被自动标记为垃圾评论? **/
40 aiDeterminedSpam: boolean
41 /** 评论中是否包含图片? **/
42 hasImages: boolean
43 /** 按 "Most Relevant"(最相关)排序方向评论所在的页码。 **/
44 pageNumber: number | null
45 /** 按 "Oldest First"(最早优先)排序方向评论所在的页码。 **/
46 pageNumberOF: number | null
47 /** 按 "Newest First"(最新优先)排序方向评论所在的页码。 **/
48 pageNumberNF: number | null
49 /** 评论是被自动还是手动批准的? **/
50 approved: boolean
51 /** 用户撰写评论时的区域设置代码(格式:en_us)。 **/
52 locale: string | null
53 /** 评论中成功解析的 @mentions。没有时为空。 **/
54 mentions: CommentUserMention[]
55 /** 评论所属的域名。 **/
56 domain: string | null
57 /** 与此评论关联的审核组 id。没有时为空。 **/
58 moderationGroupIds: string[]
59 /** 此评论中评论者名称旁显示的徽章,以事件创建时为准。没有时为空。 **/
60 badges: CommentUserBadgeInfo[]
61}
62

当用户在评论中被 @ 提及时,相关信息存储在名为 mentions 的列表中。该列表中的每个对象 具有以下结构。

Webhook Mentions 对象
Copy CopyRun External Link
1
2interface CommentUserMention {
3 /** 用户 id。对于 SSO 用户,将带有您的租户 id 前缀。 **/
4 id: string
5 /** 最终的 @mention 标签文本,包括 @ 符号。 **/
6 tag: string
7 /** 原始的 @mention 标签文本,包括 @ 符号。 **/
8 rawTag: string
9 /** 被提及的用户类型。user = FastComments.com 账户。sso = SSOUser。 **/
10 type: 'user'|'sso'
11 /** 即使用户选择不接收通知,此项仍会设置为 true。 **/
12 sent: boolean
13}
14

badges 列表中的每一项具有以下结构。徽章类型请参阅徽章指南。

Webhook 徽章对象
Copy CopyRun External Link
1
2interface CommentUserBadgeInfo {
3 /** 授予该用户的徽章 id。 **/
4 id: string
5 /** 徽章类型。 **/
6 type: number
7 /** 徽章描述,悬停在徽章上时显示。 **/
8 description: string
9 /** 徽章上显示的文本。 **/
10 displayLabel?: string | null
11 /** 徽章使用的图片(如果使用图片)。 **/
12 displaySrc?: string | null
13 backgroundColor?: string | null
14 borderColor?: string | null
15 textColor?: string | null
16 /** 应用于徽章的额外 CSS 类。 **/
17 cssClass?: string | null
18}
19

HTTP 方法

您可以在管理面板中为每种 webhook 事件类型配置 HTTP 方法:

  • Create 事件:POST 或 PUT(默认:PUT)
  • Update 事件:POST 或 PUT(默认:PUT)
  • Delete 事件:DELETE、POST 或 PUT(默认:DELETE)

由于所有请求都包含 ID,Create 和 Update 操作默认(PUT)是幂等的。重复相同的 Create 或 Update 请求不应在您那边创建重复的对象。

请求头

每个 webhook 请求包含以下请求头:

请求头描述
Content-Typeapplication/json
token您的 API Secret
X-FastComments-Timestamp请求签名时的 Unix 时间戳(秒)
X-FastComments-SignatureHMAC-SHA256 签名(sha256=<hex>)

有关验证 HMAC 签名的信息,请参阅安全与 API 令牌。

安全性与 API 令牌 Internal Link

FastComments 的 webhook 请求包含多种身份验证机制以确保安全。

发送的请求头

请求头描述
token您的 API Secret(用于向后兼容)
X-FastComments-Timestamp请求签名时的 Unix 时间戳(秒)
X-FastComments-Signature负载的 HMAC-SHA256 签名

HMAC 签名验证(推荐)

我们强烈建议验证 HMAC 签名,以确保 webhook payload 是真实且未被篡改。

Signature Format: sha256=<hex-encoded-signature>

签名的计算方法:

  1. 连接: timestamp + "." + JSON_payload_body
  2. 使用您的 API Secret 作为密钥计算 HMAC-SHA256
  3. 对结果进行十六进制编码

示例验证(Node.js)

const crypto = require('crypto');

function verifyWebhookSignature(req, apiSecret) {
    const timestamp = req.headers['x-fastcomments-timestamp'];
    const signature = req.headers['x-fastcomments-signature'];

    if (!timestamp || !signature) {
        return false;
    }

    // 验证时间戳是否近期(在 5 分钟内)
    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - parseInt(timestamp, 10)) > 300) {
        return false;  // 防止重放攻击
    }

    // 验证签名
    const payload = JSON.stringify(req.body);
    const expectedSignature = crypto
        .createHmac('sha256', apiSecret)
        .update(`${timestamp}.${payload}`)
        .digest('hex');

    return signature === `sha256=${expectedSignature}`;
}

示例验证(Python)

import hmac
import hashlib
import time
import json

def verify_webhook_signature(headers, body, api_secret):
    timestamp = headers.get('X-FastComments-Timestamp')
    signature = headers.get('X-FastComments-Signature')

    if not timestamp or not signature:
        return False

    # 验证时间戳是否近期
    now = int(time.time())
    if abs(now - int(timestamp)) > 300:
        return False

    # 验证签名
    payload = json.dumps(body, separators=(',', ':'))
    message = f"{timestamp}.{payload}"
    expected = hmac.new(
        api_secret.encode(),
        message.encode(),
        hashlib.sha256
    ).hexdigest()

    return signature == f"sha256={expected}"

示例验证(PHP)

function verifyWebhookSignature($headers, $body, $apiSecret) {
    $timestamp = $headers['X-FastComments-Timestamp'] ?? null;
    $signature = $headers['X-FastComments-Signature'] ?? null;

    if (!$timestamp || !$signature) {
        return false;
    }

    // 验证时间戳是否近期(在 5 分钟内)
    $now = time();
    if (abs($now - intval($timestamp)) > 300) {
        return false;
    }

    // 验证签名
    $payload = json_encode($body, JSON_UNESCAPED_SLASHES);
    $message = $timestamp . '.' . $payload;
    $expectedSignature = 'sha256=' . hash_hmac('sha256', $message, $apiSecret);

    return hash_equals($expectedSignature, $signature);
}

旧版身份验证

包含您 API Secret 的 token 请求头仍会出于向后兼容而发送。然而,我们建议迁移到 HMAC 验证以提高安全性,因为它可以防止重放攻击。

通过 API 管理网络钩子 Internal Link

Webhooks 也可以通过 REST API 进行管理。这正是 Zapier 等集成在不触及仪表板的情况下订阅评论事件的方式,它遵循 REST Hooks 模式:订阅、接收事件、取消订阅。

API 订阅与仪表板中配置的 Webhooks 并存。评论事件会发送到每个匹配其域名的 webhook,每个 webhook 都是一次独立的投递,无论该 webhook 是以何种方式创建的。

身份验证

每个请求都需要在 x-api-key 头部(或 API_KEY 查询参数)中提供您的 API Key,并在 tenantId 查询参数中提供租户 ID。这两项信息均显示在仪表板的 API Secret 页面上。

订阅

POST https://fastcomments.com/api/v1/webhooks?tenantId=YOUR_TENANT_ID
x-api-key: YOUR_API_KEY
Content-Type: application/json

{
    "url": "https://hooks.zapier.com/hooks/catch/123/abc",
    "event": "comment-created"
}
字段必填写描述
url是绝对的 http 或 https URL。
event是comment-created、comment-updated 或 comment-deleted。
domain否您账户配置中的域名。默认 *,表示接收所有域名的事件。
method否POST(默认)、PUT 或 DELETE。

响应中包含订阅信息:

{
    "status": "success",
    "webhook": {
        "id": "66f1c4c1e7a2b3d4f5a6b7c8",
        "url": "https://hooks.zapier.com/hooks/catch/123/abc",
        "event": "comment-created",
        "domain": "*",
        "method": "POST",
        "source": "api",
        "enabled": true,
        "createdAt": "2026-09-08T12:00:00.000Z"
    }
}

再次使用相同的 URL、相同的事件和域名下订阅时,会返回已有的订阅,而不是创建重复项,因此客户端可以安全地重试。每个租户最多可拥有 50 个 API 订阅。

列表

GET https://fastcomments.com/api/v1/webhooks?tenantId=YOUR_TENANT_ID

返回该租户的所有 webhook,包括在仪表板中管理的("source": "dashboard")。可使用 event、domain 或 source 进行过滤。

取消订阅

DELETE https://fastcomments.com/api/v1/webhooks/SUBSCRIPTION_ID?tenantId=YOUR_TENANT_ID

删除订阅的同时也会丢弃仍在队列中的任何事件。仅能删除通过 API 创建的订阅;仪表板中的 webhook 或在您账户中不存在的 ID 会返回 404,错误码为 not-found。仪表板 webhook 可在 Webhooks 页面进行编辑。

负载与签名

投递使用与仪表板 webhook 相同的负载(参见数据结构),并使用相同的 HMAC 方案进行签名(参见安全性与 API 令牌)。API 订阅永远不会收到旧版的 token 头部,请改为验证 X-FastComments-Signature 头部。

示例负载

GET https://fastcomments.com/api/v1/webhooks/sample-payloads?tenantId=YOUR_TENANT_ID&event=comment-created&limit=3

返回账户最近的评论,形状与投递时的负载完全一致,便于在首次事件到达前展示真实的示例数据。event 为可选参数,仅用于验证,因为每种事件都会投递相同的评论对象。limit 默认 3,取值范围 1 到 10。消耗 2 个 API 积分。

{
    "status": "success",
    "payloads": [
        {
            "id": "66f1c4c1e7a2b3d4f5a6b7c8",
            "urlId": "https://example.com/blog/hello-world",
            "commenterName": "Jane Reader",
            "comment": "Great article!",
            "date": "2026-09-08T12:00:00.000Z",
            "approved": true
        }
    ]
}

响应 410 Gone

如果 API 订阅的端点返回 HTTP 410 Gone,FastComments 会将其视为取消订阅:该订阅连同其排队的事件一起被删除,且不再尝试投递。仪表板中配置的 webhook 永远不会被自动删除;对它们而言 410 只是一次普通的失败。其他任何失败状态都会被重试,最终会禁用该 webhook,详见「工作原理与重试处理」章节。

仪表板

API 订阅会在 Webhooks 列表中显示,来源标记为 API,管理员可以对其进行编辑、禁用、重新启用或删除。

结论

本 Webhooks 文档到此结束。

我们希望您觉得 FastComments Webhook 集成易于理解且便于快速设置。

如果您认为已发现我们文档中的任何空白或不足,请在下方告知我们。