Skip to Content
API参考Webhooks

Webhooks(接收)

当 SupDesk 项目中发生事件时,出站 Webhooks 会向您配置的端点发送 HTTP POST 请求。本页面记录了载荷格式、签名机制和重试行为,以便您构建可靠的 Webhook 接收器。

载荷格式

每次 Webhook 投递使用标准信封:

{ "event": "post.created", "timestamp": "2026-07-15T12:00:00.000Z", "project_id": "550e8400-e29b-41d4-a716-446655440000", "data": { ... } }
字段类型描述
eventstring触发投递的事件
timestampstring事件发生时的 ISO 8601 时间戳
project_idstring事件发生的项目 UUID
dataobject事件特定的载荷(见下文)

事件

post.created

提交了新的反馈帖子。

{ "event": "post.created", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "title": "Add dark mode", "type": "feature", "status": "open", "author_type": "end_user", "body": "It would be great to have a dark mode option.", "created_at": "2026-07-15T12:00:00Z" } }

post.updated

反馈帖子已被编辑。

{ "event": "post.updated", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "title": "Add dark mode", "type": "feature", "status": "open", "author_type": "end_user", "body": "It would be great to have a dark mode option.", "created_at": "2026-07-15T12:00:00Z" } }

post.deleted

反馈帖子已被删除。

{ "event": "post.deleted", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "title": "Add dark mode", "type": "feature", "status": "open", "author_type": "end_user", "body": "It would be great to have a dark mode option.", "created_at": "2026-07-15T12:00:00Z" } }

post.status_changed

帖子状态已更改。

{ "event": "post.status_changed", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "title": "Add dark mode", "type": "feature", "old_status": "open", "new_status": "planned", "author_type": "member", "body": "We're planning this for Q3.", "created_at": "2026-07-15T12:00:00Z" } }

comment.created

帖子中添加了新评论。

{ "event": "comment.created", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "post_id": "550e8400-e29b-41d4-a716-446655440001", "post_title": "Add dark mode", "author_type": "end_user", "body": "Any update on this?", "created_at": "2026-07-15T12:00:00Z" } }

comment.updated

评论已被编辑。

{ "event": "comment.updated", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "post_id": "550e8400-e29b-41d4-a716-446655440001", "post_title": "Add dark mode", "author_type": "end_user", "body": "Any update on this?", "created_at": "2026-07-15T12:00:00Z" } }

comment.deleted

评论已被删除。

{ "event": "comment.deleted", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "post_id": "550e8400-e29b-41d4-a716-446655440001", "post_title": "Add dark mode", "author_type": "end_user", "body": "Any update on this?", "created_at": "2026-07-15T12:00:00Z" } }

message.created

发送了新的私信。

{ "event": "message.created", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "thread_id": "550e8400-e29b-41d4-a716-446655440001", "thread_subject": "Help needed", "sender": "end_user", "body": "I can't find the settings page.", "via": "web", "created_at": "2026-07-15T12:00:00Z" } }

message.updated

私信已被编辑。

{ "event": "message.updated", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "thread_id": "550e8400-e29b-41d4-a716-446655440001", "thread_subject": "Help needed", "sender": "end_user", "body": "I can't find the settings page.", "via": "web", "created_at": "2026-07-15T12:00:00Z" } }

message.deleted

私信已被删除。

{ "event": "message.deleted", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "thread_id": "550e8400-e29b-41d4-a716-446655440001", "thread_subject": "Help needed", "sender": "end_user", "body": "I can't find the settings page.", "via": "web", "created_at": "2026-07-15T12:00:00Z" } }

beta_feedback.created

Beta 测试者对 Beta 计划提交了反馈。

{ "event": "beta_feedback.created", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "program": "Acme 2.0 Beta", "category": "bug", "severity": "high", "title": "Crash on export", "body": "The app closes when I export a large file.", "created_at": "2026-07-15T12:00:00Z" } }

severity 可能为 null

beta_feedback.deleted

一条 Beta 反馈已被删除。

{ "event": "beta_feedback.deleted", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "program": "Acme 2.0 Beta", "category": "bug", "severity": "high", "title": "Crash on export", "body": "The app closes when I export a large file.", "created_at": "2026-07-15T12:00:00Z" } }

waitlist_signup.created

访客加入了候补名单。

{ "event": "waitlist_signup.created", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "email": "jamie@example.com", "status": "waiting", "position": 42, "referral_count": 0, "referral_code": "a1b2c3", "source": "portal", "created_at": "2026-07-15T12:00:00Z", "invited_at": null, "joined_at": null } }

invitedjoined 记录的 positionnull

waitlist_signup.invited

候补名单中的一条记录被从名单中邀请。

{ "event": "waitlist_signup.invited", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "email": "jamie@example.com", "status": "invited", "position": null, "referral_count": 0, "referral_code": "a1b2c3", "source": "portal", "created_at": "2026-07-15T12:00:00Z", "invited_at": "2026-07-15T12:00:00Z", "joined_at": null } }

waitlist_signup.joined

被邀请的记录接受并加入。

{ "event": "waitlist_signup.joined", "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "email": "jamie@example.com", "status": "joined", "position": null, "referral_count": 0, "referral_code": "a1b2c3", "source": "portal", "created_at": "2026-07-15T12:00:00Z", "invited_at": "2026-07-15T12:00:00Z", "joined_at": "2026-07-15T12:05:00Z" } }

csat_survey.completed

CSAT 满意度调查已以评分完成。

{ "event": "csat_survey.completed", "data": { "thread_id": "550e8400-e29b-41d4-a716-446655440000", "rating": 5, "comment": "Support was fast and friendly.", "created_at": "2026-07-15T12:00:00Z" } }

rating 是介于 1 和 5 之间的整数;comment 可能为 null

签名验证

每次投递包含一个 X-SupDesk-Signature 头:

X-SupDesk-Signature: sha256=a1b2c3d4e5f6...

sha256= 之后的值是使用 Webhook 签名密钥计算的请求体原始数据的 十六进制编码 HMAC-SHA256。

验证步骤

  1. 读取请求体原始数据(不要先解析 JSON)
  2. 计算 HMAC-SHA256(签名密钥, 原始数据)
  3. 将十六进制摘要与签名头中的值进行比较
  4. 使用恒定时间比较以防止时序攻击

Node.js 示例

import { createHmac, timingSafeEqual } from "crypto"; function verifySupDeskWebhook(secret, rawBody, signatureHeader) { const expected = createHmac("sha256", secret) .update(rawBody) .digest("hex"); const received = signatureHeader.replace("sha256=", ""); return timingSafeEqual(Buffer.from(expected), Buffer.from(received)); } // Express example app.post("/webhooks/supdesk", express.raw({ type: "application/json" }), (req, res) => { const sig = req.headers["x-supdesk-signature"]; if (!verifySupDeskWebhook(SECRET, req.body, sig)) { return res.status(401).json({ error: "Invalid signature" }); } const event = JSON.parse(req.body); // Process event... res.status(200).json({ ok: true }); });

Python 示例

import hmac import hashlib from flask import Flask, request, jsonify app = Flask(__name__) WEBHOOK_SECRET = "your-signing-secret" def verify_signature(secret: str, body: bytes, header: str) -> bool: expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest() received = header.removeprefix("sha256=") return hmac.compare_digest(expected, received) @app.route("/webhooks/supdesk", methods=["POST"]) def handle_webhook(): sig = request.headers.get("X-SupDesk-Signature", "") if not verify_signature(WEBHOOK_SECRET, request.data, sig): return jsonify(error="Invalid signature"), 401 event = request.get_json() # Process event... return jsonify(ok=True), 200

Ruby 示例

require "openssl" require "sinatra" require "json" WEBHOOK_SECRET = "your-signing-secret" def verify_signature(secret, body, header) expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body) received = header.sub("sha256=", "") Rack::Utils.secure_compare(expected, received) end post "/webhooks/supdesk" do body = request.body.read sig = request.env["HTTP_X_SUPDESK_SIGNATURE"] || "" unless verify_signature(WEBHOOK_SECRET, body, sig) halt 401, { error: "Invalid signature" }.to_json end event = JSON.parse(body) # Process event... status 200 { ok: true }.to_json end

重试

当 Webhook 投递失败时,SupDesk 以指数退避方式重试:

尝试延迟
1立即
21 秒
32 秒

重试条件: 5xx 响应、网络错误、超时(10 秒)。

不重试: 4xx 响应(客户端错误表示配置问题)。

响应 Webhook

  • 返回 2xx 状态码以确认收到
  • 10 秒内响应以避免超时重试
  • 如果逻辑耗时较长,请异步处理事件
  • 对于重复投递返回相同响应(幂等性)
Last updated on