跳到正文

接收 Webhook ​

首次接入推荐长连接 Node.js 示例:无需公网接收地址或 Webhook 加密配置。以下步骤适用于主动选择的 HTTPS Webhook。

Webhook 用于接收订阅激活、套餐变更和取消生效等事件通知。GoodVision 将事件发送到应用配置的 HTTPS 接收地址,接收端验证并持久化事件后返回成功,再异步处理业务。

事件采用“至少一次”投递,同一事件可能重复或乱序到达。

HTTP 2xx 是接收 ACK,接收端必须在返回前持久化事件。响应丢失或消费者崩溃会触发重试,接收端按 event_id 业务去重。自动重试耗尽后,在控制台查看失败投递并申请恢复,恢复仍使用原 event_id。

接入步骤 ​

  1. 配置推送:进入当前应用 → 事件与 Webhook → 添加事件,填写公网 HTTPS 回调地址,测试连接后选择事件并保存。平台自动生成签名密钥和 Token,并启用推送。
  2. 配置接收端:打开「接收端配置」,主动查看 HMAC Secret 和 Verification Token,将两项保存到服务端。接收端按下文处理连接校验、验签与可靠应答。
  3. 联调:在 Sandbox 的「事件调试」中选择已订阅事件并发送,进入「投递日志」确认接收端返回 2xx。入队不表示接收成功。
配置必填性如何处理
回调地址🔴 必填开发者填写;公网 HTTPS,可响应连接校验
订阅事件🔴 必填至少选择一个,按业务需要添加
HMAC Secret、Verification Token平台生成无需填写或自定义;查看后保存到接收端
Encrypt Key可选默认不加密正文;仅启用消息加密时配置接收端解密
同步回调可选事件推送无需配置;同步请求与响应使用独立 HTTPS 配置

多应用分别配置回调地址和事件;可以使用同一接收服务,但每个应用使用各自的凭据。接收端按应用隔离验签、消息与业务处理。事件清单见事件目录。

每个应用仅支持一个未撤销的 Webhook 接收地址。异步事件 event_types[] 和同步回调 callback_types[] 分别配置,修改其中一项时保留另一项的完整配置。

商业订阅、API 调用授权和 Webhook Subscription 分别管理。取消商业订阅不会撤销 API 授权或停止 Webhook。

保存配置失败后的恢复 ​

事件、接收地址或安全配置保存结果未知时,配置窗口保留原内容、版本和操作编号,并暂停编辑和关闭窗口。使用“恢复原请求”确认结果,不重新创建一次保存或轮换。

恢复时若要求登录或身份验证,完成后回到同一应用,再明确恢复原请求。验证不会自动提交,临时显示的凭据会清除。若页面明确提示凭据无法再次交付,读取当前凭据版本,核对后重新确认凭据操作;不要索取旧明文。

若页面明确提示“Webhook 权限需要重新核验”,临时凭据和旧确认会清除。点击“重新核验 Webhook 权限”读取当前应用权限和配置;读取暂时失败时可再次核验。成功后只提供当前允许的操作,不恢复旧明文或旧确认。后续保存或轮换需重新填写并确认;重核本身不会提交配置。

验证接收地址 ​

首次创建 Webhook 或修改 Endpoint 地址时,请先在 Developer Console 点击“测试连接”。平台会向候选地址发送一次校验请求,接收端必须在 1 秒内原样返回 challenge:

http
POST /webhooks/goodvision
Content-Type: application/json
X-GV-Webhook-Verification: url_verification

{"type":"url_verification","challenge":"opaque_challenge","token":"opaque_verification_token"}
http
HTTP/1.1 200 OK
Content-Type: application/json

{"challenge":"opaque_challenge"}

接收地址和响应需满足以下要求:

项目要求
接收地址公网可访问的 HTTPS URL
HTTP 状态2xx,不重定向
Content-Typeapplication/json
Body不超过 64 KiB,仅包含 challenge

测试通过后,控制台使用有效期 5 分钟的校验证明(proof)保存配置。该证明绑定当前企业、应用和完整 Endpoint URL。证明过期或 URL 变化时,重新测试。

历史 Webhook 缺少校验状态时,在「编辑 Endpoint」中补测。地址已验证且 URL 未变化时,增删事件或回调无需再次测试。

校验请求中的 token 与事件投递使用的 Verification Token 不是同一类凭据。challenge、token 和 proof 都是临时安全材料,不得记录、复用,也不得写入日志、埋点、工单或持久化业务数据。

在控制台调试完整事件 ​

在当前 Sandbox 应用的「事件与 Webhook → 事件调试」中:

  1. 选择已订阅的生命周期事件。
  2. 点击「发送测试事件」。
  3. 查看投递日志,检查接收结果。

平台使用当前 Endpoint、HMAC Secret 和 Verification Token 发送完整的合成订阅摘要。启用加密时使用当前 Encrypt Key。支持事件目录中的全部 19 种订阅生命周期事件。

data.product_code 使用当前应用绑定的产品编码;没有绑定时使用合成值 webhook_test_product。它不表示任何真实产品已经开通。每次测试都有独立事件和合成资源 ID,不模拟同一订阅的连续状态变化;取消预约到生效等连续场景用下文和生命周期指南中的样例验证。

测试要求你拥有当前应用的 Webhook 管理权限,Application 与唯一 Webhook 均已生效,签名凭据已交付,Endpoint 已通过连接校验。Production、未验证、未生效或历史配置冲突时,控制台说明原因并停止发送。

「测试已入队」表示平台已接受请求。打开「投递日志」,点击「重新读取投递」,用返回的 Delivery ID、Event ID 和 Correlation ID 查找记录。

响应检查项
204已可靠接收
401/403签名、时间窗、Nonce 和 Token
422Schema 和解密后的字段
5xx 或网络错误接收端是否可用及其所需服务

测试仅尝试一次。修复后发送新的测试事件;测试失败不会暂停正常 Webhook。

先完成正常验签,再检查签名正文的 event_id 是否以 evt_webhook_test_ 开头。X-GV-Test-Delivery: true 仅作提示,不能单独用于判断测试请求。

测试中的客户、订阅、租户、套餐和操作均为合成资源。请在单独的测试分支记录结果并应答,不更新真实客户订阅或成员权限,也不使用这些合成 ID 查询真实业务 API。

调试请求只选择 event_type 与当前配置版本;不接受自定义 Endpoint、Secret、Token 或业务正文。接收端看到的 producer 使用为应用签发的实际来源值,与下面静态示例的展示占位不同。

完整示例:订阅已激活 ​

客户在平台客户工作区完成订阅流程后,收到 oem.subscription.activated.v1。下面是未启用加密时的完整请求,data 包含订阅摘要的全部必需字段,而不是仅包含发生变化的字段:

也可下载完整 HTTP 样例。Content-Length 按 Body 的 UTF-8 字节数计算;修改正文时需要重新计算长度与签名。

http
POST /webhooks/goodvision HTTP/1.1
Host: receiver.example.com
Content-Type: application/json
Content-Length: 1185
X-GV-Delivery-ID: delivery_demo_activation
X-GV-Event-ID: evt_demo_activated
X-GV-Event-Type: oem.subscription.activated.v1
X-GV-Request-Timestamp: 1791244800
X-GV-Request-Nonce: nonce_demo_activation_001
X-GV-Signature: sha256=675ba54e260ff141e61a1090a6b4277710fc9b80e4205b55f4d0dc86fee56d53

{
  "event_id": "evt_demo_activated",
  "event_type": "oem.subscription.activated.v1",
  "schema_version": "1",
  "occurred_at": "2026-10-06T00:00:00Z",
  "producer": "opaque",
  "tenant_id": "org_customer_demo",
  "resource_id": "sub_demo_cancel",
  "resource_version": 2,
  "correlation_id": "req_demo_activation",
  "data": {
    "id": "sub_demo_cancel",
    "organization_id": "org_customer_demo",
    "open_platform_application_id": "app_demo_saas",
    "environment": "sandbox",
    "product_tenant_id": "tenant_demo_cancel",
    "party_id": "party_customer_demo",
    "order_id": "order_demo_initial",
    "product_code": "relaydesk",
    "plan_code": "basic_monthly_demo",
    "plan_version": 1,
    "status": "active",
    "current_period_start": "2026-10-06T00:00:00Z",
    "current_period_end": "2026-11-06T00:00:00Z",
    "status_effective_at": "2026-10-06T00:00:00Z",
    "payment_reference": "payment_demo_initial",
    "operation_id": "op_demo_activation",
    "reason_code": "example_activation",
    "resource_version": 2,
    "created_at": "2026-10-05T23:55:00Z",
    "updated_at": "2026-10-06T00:00:00Z"
  },
  "verification_token": "demo-verification-token-only"
}

本例的 ID、套餐和原因码均为合成值。producer: "opaque" 是来源占位值,实际枚举请使用当前应用的事件 Schema。reason_code 也按当前 Schema 解释,示例值不可用于实际业务判断。

签名使用公开演示值 demo-signing-secret-only,Verification Token 使用 demo-verification-token-only;二者不是有效凭据。签名覆盖示例 Body 的 UTF-8 字节,包括最后一个 LF 换行。固定时间戳只用于复算,今天发送会因超出时间窗被拒绝。

服务端校验通过、Inbox 事务提交后,返回空 Body:

http
HTTP/1.1 204 No Content

这表示“已经可靠接收”,不表示已经完成套餐或权限更新。响应正文不用于返回业务处理结果。重复业务事件在验证请求合法后也返回 204。

该事件表示商业订阅已进入 active。开放成员功能前,还需通过 oem.access_context.changed.v1 和已授权查询确认成员权限,详见生命周期中的权限处理。

请求头与签名 ​

Header用途
X-GV-Delivery-ID投递记录标识,用于查询投递、重试和接收日志
X-GV-Event-ID稳定业务事件 ID,用于业务去重
X-GV-Event-Type带版本的事件名称
X-GV-Request-Timestamp当前投递尝试的 Unix 秒时间戳
X-GV-Request-Nonce当前投递尝试的随机值,用于拒绝重放
X-GV-Test-Delivery仅调试投递发送 true;验签后仍须核对正文测试前缀
X-GV-Signaturesha256=<hex> 格式的 HMAC-SHA256

签名输入严格为:

text
signing_input = timestamp + "\n" + nonce + "\n" + raw_body
expected = "sha256=" + hex(HMAC-SHA256(signing_secret, signing_input))

raw_body 是 HTTP 收到的原始字节。计算签名前不要解析、格式化或重新序列化 JSON。

  1. 检查时间戳是否在前后 5 分钟窗口内。
  2. 在共享存储中原子登记 Nonce,至少保留 10 分钟,拒绝重复值。
  3. 使用常量时间比较验证签名。

多副本接收端需共享 Nonce 存储,单进程内存无法防止跨副本重放。

接收端校验顺序 ​

  1. 读取原始 Body,检查投递 ID、事件 ID、事件类型、时间戳、Nonce 和签名 Header 都存在且格式合法。
  2. 校验时间窗并原子登记 Nonce;过期或重复请求返回 401,Nonce 存储不可用返回 503。
  3. 用当前签名 Secret 对签名输入计算 HMAC-SHA256,并做常量时间比较;失败返回 401。
  4. 按当前 Endpoint 配置解析明文,或解密 encrypt;两种模式都必须常量时间校验 verification_token,失败返回 401。
  5. 移除传输字段 verification_token 后,按已签发事件 Schema 验证信封。确认 Header 事件 ID、事件类型与 Body 一致;订阅事件还需核对客户 Organization、Application、环境、产品、资源 ID 和版本,失败返回 422。
  6. 以 event_id 幂等写入持久化 Inbox,保存去掉 Token 的事件和 delivery_id、correlation_id;写入失败返回 503。后台以 resource_version 防止旧事件覆盖新状态。
  7. Inbox 事务提交后立即返回 2xx,耗时业务逻辑由后台 Worker 执行。
text
raw_body = request.raw_body
delivery_id = request.headers["X-GV-Delivery-ID"]
event_id = request.headers["X-GV-Event-ID"]
event_type = request.headers["X-GV-Event-Type"]
timestamp = request.headers["X-GV-Request-Timestamp"]
nonce = request.headers["X-GV-Request-Nonce"]
reject 401 unless timestamp_within_window(timestamp, 5_minutes)
reject 503 if nonce_store_unavailable
reject 401 unless reserve_nonce_once(nonce, 10_minutes)
expected = "sha256=" + hex(hmac_sha256(signing_secret, timestamp + "\n" + nonce + "\n" + raw_body))
reject 401 unless constant_time_equal(expected, request.headers["X-GV-Signature"])

transport = parse_json(raw_body)
if endpoint.encryption_enabled
  reject 422 unless transport has only the encrypt field
  transport = parse_json(decrypt_aes256cbc(transport.encrypt, encrypt_key))
end
reject 401 unless constant_time_equal(verification_token, transport.verification_token)
event = transport without verification_token
reject 422 unless validate_issued_event_schema(event)
reject 422 unless event.event_id == event_id
reject 422 unless event.event_type == event_type
reject 422 unless event.resource_id == event.data.id
reject 422 unless event.resource_version == event.data.resource_version
if event.event_id starts_with "evt_webhook_test_"
  reject 422 unless matches_configured_sandbox_application_product(event)
  reject 503 unless commit_test_inbox_if_new(event.event_id, event, delivery_id, event.correlation_id)
  return 204 // only record test results; never apply customer state or permissions
end
reject 422 unless matches_configured_customer_application_environment_product(event)
reject 503 unless commit_inbox_if_new(event.event_id, event, delivery_id, event.correlation_id)
return 204

这是订阅事件接收伪代码。commit_inbox_if_new 必须在事务中按唯一键区分“新事件提交成功”“相同事件已接收”和“存储失败”;前两种返回成功。校验连接请求走前面的独立 challenge 分支,不作为订阅事件入库。

明文与加密 Body ​

凭据用途
HMAC Secret对外层 HTTP 原始 Body 验签
Verification Token检查解析后完整事件中的 verification_token
Encrypt Key仅用于启用加密时的事件解密

三类凭据分别用于签名、Token 校验和解密。

明文 Body 包含完整事件信封及顶层 verification_token。先验证并移除 Token,再检查事件 Schema,否则封闭 Schema 会将其作为未知字段拒绝。Token 不保存到 Inbox、普通日志或工单。

启用加密时,完整 JSON 仍包含 Verification Token,外层只有下面一个字段。这是格式示意,尖括号内需要换成真实 Base64:

json
{
  "encrypt": "<Base64 of IV followed by ciphertext>"
}

按以下顺序解密:

  1. 对外层原始 Body 验签。
  2. Base64 解码,取前 16 字节为 IV,其余为密文。
  3. 使用 SHA256(UTF-8 Encrypt Key) 作为 AES-256-CBC Key 解密,并移除 PKCS#7 Padding。
  4. 解析 JSON,常量时间校验 Token。
  5. 移除 Token,再验证事件 Schema。

严格使用 Endpoint 配置的传输模式。解密失败时拒绝处理,不能回退到明文。

本地复算签名 ​

下载激活事件 JSON与离线脚本,放到同一目录,在 Node.js 18 或更高版本执行:

bash
node "webhook-signature.mjs"

脚本使用公开演示凭据、当前时间和随机 Nonce,输出包含全部请求头和 Body 的合成请求,不会发起网络请求。可用它核对原始字节的签名计算。

通过实际事件 Schema 校验前,替换 producer 和接入配置为当前应用的值。正式接收端还需实现时间窗、Nonce、Token、Schema、范围检查和持久化。

js
import { createHmac, randomBytes } from "node:crypto";
import { readFileSync } from "node:fs";

// Offline example only: these public demo credentials are never valid credentials.
const signingSecret = "demo-signing-secret-only";
const verificationToken = "demo-verification-token-only";
const event = JSON.parse(readFileSync(new URL("./subscription-activated.json", import.meta.url), "utf8"));
const timestamp = String(Math.floor(Date.now() / 1000));
const nonce = randomBytes(16).toString("hex");
const rawBody = Buffer.from(JSON.stringify({ ...event, verification_token: verificationToken }), "utf8");
const signature = createHmac("sha256", signingSecret)
  .update(`${timestamp}\n${nonce}\n`, "utf8")
  .update(rawBody)
  .digest("hex");

// Print a synthetic request; do not send it or log a real event body.
process.stdout.write([
  "POST /webhooks/goodvision HTTP/1.1",
  "Host: receiver.example.com",
  "Content-Type: application/json",
  `Content-Length: ${rawBody.length}`,
  "X-GV-Delivery-ID: delivery_demo_activation",
  `X-GV-Event-ID: ${event.event_id}`,
  `X-GV-Event-Type: ${event.event_type}`,
  `X-GV-Request-Timestamp: ${timestamp}`,
  `X-GV-Request-Nonce: ${nonce}`,
  `X-GV-Signature: sha256=${signature}`,
  "",
  rawBody.toString("utf8"),
].join("\r\n"));

去重与乱序 ​

  • 唯一约束使用 event_id,不要使用 delivery_id 或 correlation_id。
  • 同一投递记录重试时 delivery_id、event_id、correlation_id 保持不变;每次尝试重新生成时间戳、Nonce 和加密 IV,并重新签名。
  • 重复事件返回成功,但不能重复创建订单、取消订阅或提交支付/退款操作。
  • 当前资源版本为 N 时,只直接应用 N+1;N+2 先保存为待处理缺口。
  • 缺失事件长期未到达时,调用契约授权的状态查询接口完成对账。
收到的情况HTTP 响应示例后台行为
新事件,校验通过并成功提交 Inbox204异步处理一次
新 Nonce 的合法重试,event_id 已接收204不重复业务操作
同一 Nonce 被重放、时间窗过期、签名或 Token 错误401不入库、不执行业务
不支持的 Schema、Header/Body 不一致或接入范围不匹配422拒绝并检查事件格式与应用配置
Inbox 暂时不可写或 Nonce 存储不可用503不确认接收,等待平台重试
当前版本为 2,先收到版本 4提交 Inbox 后 204保存缺口,等版本 3 或授权回查,不能直接覆盖

delivery_id 用于查看投递记录,event_id 用于业务去重,correlation_id 用于关联排障。完整去掉 Token 的事件只保存在受控 Inbox,不进入浏览器日志、普通应用日志或工单。

签名密钥与轮换 ​

  • Webhook 签名密钥只用于当前 Subscription 的 HMAC 验签,不能与 OAuth Client Secret、API Key 或其他 Application 共用。
  • 新版本签名密钥安全交付成功后才会生效;交付失败时旧版本继续有效。控制台只显示版本和交付状态,不显示密钥正文。
  • 接收方在轮换窗口内可以同时校验当前版本和上一版本,完成切换后及时移除旧版本。
  • 明文密钥不得进入 URL、日志、埋点、缓存、工单或普通数据库字段。

失败与恢复 ​

投递结果处理方式
网络错误、连接超时、408、425、429 或约定的 5xx平台自动重试,业务事件默认最多尝试 8 次
400、401、403、404、410、422先检查接收地址、证书、密钥和 Schema
连续失败保留失败记录,并可能暂停订阅;在开发者控制台排查和恢复
控制台合成测试失败仅尝试一次,修复后发送新测试

修复接收地址或验签配置后,先查询当前状态核对,再通过支持渠道申请按原 event_id 重新投递。重投使用原记录和事件内容,具有幂等保护并保留审计记录,不创建新业务事件。此操作由平台处理,第三方无需调用管理接口。

同步回调可用性 ​

独立请求、响应示例和结果未知处理见处理同步回调。

开发者控制台可为同一 Endpoint 配置 token.agent.action.v1,在 Sandbox 的“回调配置”发送单次合成测试并查看独立结果。真实 TOKEN 业务请求方尚未接入,保存或测试成功不表示生产业务已调用。

接收端须按已签发的请求/响应 Schema 验签、按当前配置解密并在 5 秒内返回结构化结果。同步回调采用独立处理规则,不使用异步事件的重试与失败投递恢复;结果未知时不自动重试,核对结果后再显式发起新测试。

从控制台安全取得签名密钥 ​

使用控制台交付的 Webhook,可在“接收端配置”主动点击“临时查看 HMAC Secret”。应用所有者或获准查看的协作者经当前登录取得当前版本,随后将密钥保存到接收服务端。离开页面、切换应用/企业、查看权限或签名密钥版本变化会清除临时值。外部交付通道仍按原方式取得密钥。密钥进入交付通道不表示接收端已配置,也不表示事件已接收;配置后请通过事件调试和投递日志验证。

成员事件与登录用户关联 ​

成员相关的 oem.access_context.changed.v1 使用 data.subject_id 对应用户上下文的 space.membership_id,不能与 sub 或用户别名互换。订阅记录按应用、环境、客户空间及 subscription ID 关联,不绑定最后登录用户。

开发者文档。第三方接入不需要内部运营平台权限。