Z Tag
管理组织

Webhook 接入指南(发送方)

拿到接收地址后,配置 GitLab 或编写 HTTP 推送,完成验签、事件匹配、重试与联调。

适用于对接 Z Tag 的系统开发者

本页面向维护 GitLab、监控、CI 或业务系统的人员:你已拿到 Z Tag 的 Webhook 地址,需要让系统向它推送事件。发送请求使用该地址对应的凭证,无需登录 Z Tag 控制台。创建地址、配置事件定义和权限见Webhook 与事件。

接入前确认

请向提供地址的人确认以下信息:

信息用途
完整的 HTTPS 接收地址按原样保存和使用;URL Token 模式的凭证已经包含在地址中
发送方类型和验签方式GitLab 或通用发送方;使用创建时选定的一种验签方式
Token 或签名密钥请求头 Token 与 HMAC 密钥用途不同,不能互换使用方式
事件名称、可选动作和样例正文与 Z Tag 中的事件定义约定一致,确保推送能匹配到事件
联调联系人和接收事件的频道或群聊由能管理该 Webhook 的人确认地址已启用、事件已定义,并且接收事件的频道或群聊已订阅

凭证在创建或轮换时只显示一次,轮换后旧凭证立即失效。实际发送程序应从环境变量或系统的凭证配置中读取;不要把凭证或含 Token 的完整地址写入代码仓库、工单或日志。下面的占位符都需要替换为你收到的信息或本次事件的数据。

从 GitLab 推送

  1. 打开 GitLab 项目的 Settings → Webhooks,在 URL 中填入完整接收地址。
  2. 如果使用默认的请求头 Token 验证,把收到的 Token 填入 Secret token。GitLab 会自动发送 X-Gitlab-Token,不需要自行计算 HMAC。如果提供的是 URL Token 模式地址,直接使用完整地址,Secret token 可留空。
  3. 勾选需要推送的事件。例如,要处理合并请求,就启用 Merge request events。保持 SSL 验证开启,保存配置。
  4. 使用 GitLab 的 Test 发送对应事件,并按下文的联调步骤确认结果。测试会产生真实的事件,订阅了它的 ZAI 会真的开始工作,请先与接收事件的频道或群聊约定测试内容。

GitLab 直接发送原生请求体即可。例如,合并请求事件的 X-Gitlab-Event 为 Merge Request Hook,正文中的 object_kind 为 merge_request,创建动作通常为 object_attributes.action: "open"。Z Tag 的事件定义可以匹配这个事件与动作,并将它命名为 gitlab.merge_request.open 供订阅。

这个订阅用的事件 key 由 Z Tag 管理侧定义;GitLab 的事件名称和原生正文不需要改成它。Z Tag 使用 GitLab 的 X-Gitlab-Event-UUID 去重,经过中间转发服务时应保留这个请求头。

GitLab 推送超过接收地址的每分钟额度(见去重、重试和大小限制)时,Z Tag 返回 HTTP 503 和 Retry-After,而不是通用地址的 429;超出的推送不会被接收。这是因为 GitLab 开启 Webhook 自动停用时,连续失败四次就会停用这个 Webhook:5xx 只是暂时停用,从一分钟起逐次延长、最长 24 小时,到期自动恢复,停用期间不发送事件;4xx 被视为配置错误,停用到有人手动重新启用。Token 错误(401)、地址错误(404)和地址已撤销(410)仍返回 4xx,修正配置后用 Test 成功发送一次即可重新启用。

GitLab 不读取响应头,也就不会按 Retry-After 等待。超出额度的推送以 503 记录在该 Webhook 编辑页的 Recent events 中(保留最近两天),需要补发时打开 View details,选择 Resend Request。

从通用系统推送

向完整接收地址发送 POST。建议使用 UTF-8 编码、不带 BOM 的 JSON,设置 Content-Type: application/json; charset=utf-8;正文建议不超过 256 KiB(编码后的字节数),见大小限制。签名按实际发送的原始字节计算;Z Tag 按 UTF-8 解析正文,非 UTF-8 编码(如 GBK)的内容在入站记录、事件匹配和交给 ZAI 的内容中都会变成乱码。

通用入口没有强制统一的业务 JSON 结构。你需要与配置事件定义的人约定事件名称、动作和业务字段;下面用“发布失败”演示一套可直接联调的约定。

准备正文和事件定义

把以下内容保存为 payload.json,将占位符替换为实际业务数据:

{
  "event": "deployment",
  "action": "failed",
  "id": "<本次事件唯一标识>",
  "occurred_at": "<带时区的 ISO 8601 发生时间>",
  "data": {
    "service": "<服务名称>",
    "environment": "<环境名称>",
    "version": "<发布版本>",
    "message": "<失败原因>",
    "url": "<详情页面地址>"
  }
}

对应的 Z Tag 事件定义选择 按发送方事件名,事件名填 deployment,动作填 failed,事件 key 可以设为 deployment.failed。接收事件的频道或群聊订阅这个 key 后,匹配的推送才会触发任务。

字段或请求头Z Tag 如何使用
X-Ztag-Event优先作为发送方事件名;本例为 deployment
正文 event / type未提供非空的 X-Ztag-Event 时,依次读取非空字符串 event、type
正文 action事件定义配置了动作时,用它进行匹配;本例为 failed
X-Ztag-Delivery-Id标识一次业务事件,用于去重;同一事件的重试保持不变
正文 id、occurred_at、data本例建议的业务字段,Z Tag 不强制校验,也不会用正文 id 自动去重

普通事件名与动作匹配不区分大小写,仍建议双方约定固定拼写。请求头和正文都填写事件名时应保持一致。来源事件名与 Z Tag 的事件 key 是两个配置项,正文中放一个 eventKey 不会自动创建事件定义或订阅。

occurred_at 表示业务发生时间,需作为业务数据显式提供;它不会自动替换 Z Tag 记录的接收时间。字段结构变化时,应同步确认事件定义或分类器仍能识别。

使用请求头 Token

仅适用于验签方式为 请求头 Token 的通用地址(Token 放在 X-Ztag-Webhook-Token 请求头)。以下命令使用 Bash 和 curl:

WEBHOOK_URL='<完整接收地址>'
WEBHOOK_TOKEN='<收到的 Token>'
DELIVERY_ID='<本次事件唯一标识>'

curl --silent --show-error --include \
  --request POST "$WEBHOOK_URL" \
  --header 'Content-Type: application/json; charset=utf-8' \
  --header "X-Ztag-Webhook-Token: $WEBHOOK_TOKEN" \
  --header 'X-Ztag-Event: deployment' \
  --header "X-Ztag-Delivery-Id: $DELIVERY_ID" \
  --data-binary @payload.json

--data-binary 会按文件内容发送正文。实际系统中的 Token 应由凭证配置注入,不要将填入真实凭证的命令保留在共享脚本或终端历史中。

使用 HMAC-SHA256 签名

这是通用地址的默认验签方式。用收到的密钥原文作为 HMAC key,不做 Base64 解码,也不先对密钥求哈希。将 SHA-256 摘要编码为十六进制,加上 sha256= 前缀,放入 X-Ztag-Signature。

方式参与签名的内容
携带 X-Ztag-Timestamp(推荐)时间戳请求头的原文、一个英文句点 .、实际发送的原始正文,按此顺序连接
不携带时间戳仅实际发送的原始正文

时间戳支持 Unix 秒或毫秒,与 Z Tag 接收时间相差不能超过五分钟。签名后必须发送同一份正文:重新序列化 JSON、改变空格、换行或字符编码都会改变签名。

以下示例需要 Node.js 22 或更高版本。保存为 send-webhook.mjs,与 payload.json 放在同一工作目录;先通过运行环境设置 WEBHOOK_URL、WEBHOOK_SECRET 和 DELIVERY_ID,再运行 node send-webhook.mjs。

import { createHmac } from "node:crypto";
import { readFile } from "node:fs/promises";

const url = process.env.WEBHOOK_URL;
const secret = process.env.WEBHOOK_SECRET;
const deliveryId = process.env.DELIVERY_ID;
if (!url || !secret || !deliveryId) {
  throw new Error("请设置 WEBHOOK_URL、WEBHOOK_SECRET 和 DELIVERY_ID");
}

const body = await readFile("payload.json");
if (body.byteLength > 256 * 1024) {
  throw new Error("请将请求正文控制在 256 KiB 以内");
}
const timestamp = String(Math.floor(Date.now() / 1000));
const signature = createHmac("sha256", secret)
  .update(`${timestamp}.`, "utf8")
  .update(body)
  .digest("hex");

const response = await fetch(url, {
  method: "POST",
  redirect: "error",
  headers: {
    "Content-Type": "application/json; charset=utf-8",
    "X-Ztag-Event": "deployment",
    "X-Ztag-Delivery-Id": deliveryId,
    "X-Ztag-Timestamp": timestamp,
    "X-Ztag-Signature": `sha256=${signature}`,
  },
  body,
  signal: AbortSignal.timeout(30_000),
});

console.log(response.status, await response.text());
const retryAfter = response.headers.get("Retry-After");
if (retryAfter) console.log("Retry-After:", retryAfter);
if (!response.ok) process.exitCode = 1;

这是一次发送示例,不会自动重试。每次运行都会重新计算时间戳和签名;重试同一事件时保持 DELIVERY_ID 和业务正文不变,并检查响应含义,包括 202 响应中的错误。

签名对不上时,先用下面的测试向量核对自己的签名实现。三项输入都按原样参与计算,正文不含末尾换行:

输入或结果值
密钥test-key
X-Ztag-Timestamp1767225600
正文{"event":"deployment","action":"failed"}
携带时间戳时的 X-Ztag-Signaturesha256=0a67c2a2f14735304a7d384375cb0fba4687d205456cc69fecdacf219a93c248
不携带时间戳时的 X-Ztag-Signaturesha256=69083674270b3ff9e6d4a2dafe681173e780dc03dfb2c4bd482ab8f4254294e5

也可以在终端里用 OpenSSL 复算携带时间戳的签名,输出的十六进制串加上 sha256= 前缀后应与上表一致:

printf '%s' '1767225600.{"event":"deployment","action":"failed"}' \
  | openssl dgst -sha256 -hmac 'test-key'

测试向量只用于核对算法:它的时间戳早已超出允许的偏差,不要发到真实接收地址。

使用 URL Token

这种方式适合无法设置认证请求头的系统。直接向收到的、已经包含 Token 的完整地址 POST 正文,无需追加 Token 或签名请求头,也不要把 Token 改放到查询参数中。能够设置自定义请求头时,仍建议发送 X-Ztag-Delivery-Id;只能配置正文时,可用 event 或 type 表达事件名,但去重会退回下文的正文哈希规则。

理解响应

首次接收成功通常返回 HTTP 202:

{
  "receiptId": "<入站记录 ID>",
  "duplicate": false
}

这表示请求已记录,Z Tag 将继续匹配事件和投递,不表示已经命中事件定义,也不表示 ZAI 已经处理成功。发送程序需要同时检查 HTTP 状态和响应体。

HTTP 状态响应或含义发送方处理
202包含 receiptId、duplicate: false已接收,保存记录 ID 供联调查询
200包含原 receiptId、duplicate: true已接收过,停止重复发送
202error: "endpoint_paused"地址已暂停,本次请求被丢弃,不会保留等待恢复;联系管理方
401missing_credential / invalid_credential核对地址的验签方式、凭证及签名正文,确认是否发生了轮换
401stale_timestamp校准发送机器时钟,重新生成时间戳和签名
401key_unavailable联系管理方检查该地址的签名密钥配置
404地址不存在或路径错误重新核对完整地址
405请求方法不支持改用 POST
410endpoint_revoked地址已撤销,停止发送并向管理方获取新地址
413请求体过大缩小正文,见大小限制
429rate_limited,附带 Retry-After: 60等待指定秒数后再试
5xx / 网络超时本次接收结果不确定按下文规则重试,保留同一投递 ID

去重、重试和大小限制

  • 每个新事件使用不同的投递 ID。通用系统用 X-Ztag-Delivery-Id,GitLab 用 X-Gitlab-Event-UUID。建议使用不超过 200 个字符的非敏感标识,例如 UUID。同一地址收到相同 ID 时,即使正文改变,也会被视为重复。
  • 同一事件的重试复用 ID。如果请求超时,Z Tag 可能已经接收成功;换一个 ID 会产生第二次投递。HMAC 重试需要更新签名时间戳和签名,业务正文保持不变。
  • 未提供投递 ID 时,只能按正文去重。同一地址在同一个固定十分钟时间桶内,相同正文会被视为重复;跨时间桶可能再次接收。不同业务事件若正文相同也可能被合并,正文里的 id 字段不能替代投递 ID 请求头。
  • 去重依赖保留中的入站记录。入站记录默认保留 14 天,记录清理后不再提供对应的去重依据。需要长期防重的业务,应在业务处理侧另行保证幂等。
  • 每个地址每分钟最多接收 120 次通过验签的请求,包含重试。验签失败的请求另计每分钟 120 次,超出后返回 429,不占用通过验签的额度。收到 429 时遵守 Retry-After;遇到超时或 5xx,采用带随机延迟的指数退避并设置重试上限。401、404、405、410 应先修正配置,暂停的地址应与管理方约定恢复后的补发,避免无限重试。
  • 正文保持在 256 KiB 以内。超出的内容会被截断保存,可能导致验签失败或无法解析 JSON;请求体远大于这个值时直接返回 413,不留入站记录。不要把截断保存当作大报文支持;文件、完整日志或大对象应放在业务系统中,事件只携带摘要、标识和链接。

完成联调

  1. 请管理方确认 Webhook 状态为 已启用(没有暂停或撤销),配置与样例一致的事件定义,并让接收事件的频道或群聊完成事件订阅。约定测试内容,确认可以产生真实任务。
  2. 从实际发送系统推送一个小事件,记录发送时间、投递 ID、HTTP 状态和响应体。确认得到 202、receiptId 和 duplicate: false,且没有 error。
  3. 用同一投递 ID 和正文重试一次。应返回 200、同一个 receiptId 和 duplicate: true,不再产生新的事件。
  4. 请管理方在控制台的 入站记录 中按发送时间找到这次请求(也可以在能使用该 Webhook 的频道或群聊里请 ZAI 按 receiptId 查询),确认正文完整、事件定义命中;再检查 事件记录 和 订阅 中的投递状态,最后在接收事件的频道或群聊里确认任务结果。

控制台的 发送测试事件 用于验证样例分类和投递,会跳过外部请求的验签;它不能代替发送系统的网络连通、凭证与签名联调。

如果请求已接收但没有任务,先检查事件名、动作和正文是否与事件定义一致,再检查事件定义与订阅是否启用、接收事件的频道或群聊是否仍能使用该 Webhook。反馈问题时提供 receiptId、投递 ID、发送时间和脱敏后的请求样例即可,不要附上 Token、签名密钥或含 Token 的完整地址。

本页目录