Webhook 接入指南(发送方)
拿到接收地址后,配置 GitLab 或编写 HTTP 推送,完成验签、事件匹配、重试与联调。
本页面向维护 GitLab、监控、CI 或业务系统的人员:你已拿到 Z Tag 的 Webhook 地址,需要让系统向它推送事件。发送请求使用该地址对应的凭证,无需登录 Z Tag 控制台。创建地址、配置事件定义和权限见Webhook 与事件。
接入前确认
请向提供地址的人确认以下信息:
| 信息 | 用途 |
|---|---|
| 完整的 HTTPS 接收地址 | 按原样保存和使用;URL Token 模式的凭证已经包含在地址中 |
| 发送方类型和验签方式 | GitLab 或通用发送方;使用创建时选定的一种验签方式 |
| Token 或签名密钥 | 请求头 Token 与 HMAC 密钥用途不同,不能互换使用方式 |
| 事件名称、可选动作和样例正文 | 与 Z Tag 中的事件定义约定一致,确保推送能匹配到事件 |
| 联调联系人和接收事件的频道或群聊 | 由能管理该 Webhook 的人确认地址已启用、事件已定义,并且接收事件的频道或群聊已订阅 |
凭证在创建或轮换时只显示一次,轮换后旧凭证立即失效。实际发送程序应从环境变量或系统的凭证配置中读取;不要把凭证或含 Token 的完整地址写入代码仓库、工单或日志。下面的占位符都需要替换为你收到的信息或本次事件的数据。
从 GitLab 推送
- 打开 GitLab 项目的 Settings → Webhooks,在 URL 中填入完整接收地址。
- 如果使用默认的请求头 Token 验证,把收到的 Token 填入 Secret token。GitLab 会自动发送
X-Gitlab-Token,不需要自行计算 HMAC。如果提供的是 URL Token 模式地址,直接使用完整地址,Secret token 可留空。 - 勾选需要推送的事件。例如,要处理合并请求,就启用 Merge request events。保持 SSL 验证开启,保存配置。
- 使用 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-Timestamp | 1767225600 |
| 正文 | {"event":"deployment","action":"failed"} |
携带时间戳时的 X-Ztag-Signature | sha256=0a67c2a2f14735304a7d384375cb0fba4687d205456cc69fecdacf219a93c248 |
不携带时间戳时的 X-Ztag-Signature | sha256=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 | 已接收过,停止重复发送 |
| 202 | error: "endpoint_paused" | 地址已暂停,本次请求被丢弃,不会保留等待恢复;联系管理方 |
| 401 | missing_credential / invalid_credential | 核对地址的验签方式、凭证及签名正文,确认是否发生了轮换 |
| 401 | stale_timestamp | 校准发送机器时钟,重新生成时间戳和签名 |
| 401 | key_unavailable | 联系管理方检查该地址的签名密钥配置 |
| 404 | 地址不存在或路径错误 | 重新核对完整地址 |
| 405 | 请求方法不支持 | 改用 POST |
| 410 | endpoint_revoked | 地址已撤销,停止发送并向管理方获取新地址 |
| 413 | 请求体过大 | 缩小正文,见大小限制 |
| 429 | rate_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,不留入站记录。不要把截断保存当作大报文支持;文件、完整日志或大对象应放在业务系统中,事件只携带摘要、标识和链接。
完成联调
- 请管理方确认 Webhook 状态为 已启用(没有暂停或撤销),配置与样例一致的事件定义,并让接收事件的频道或群聊完成事件订阅。约定测试内容,确认可以产生真实任务。
- 从实际发送系统推送一个小事件,记录发送时间、投递 ID、HTTP 状态和响应体。确认得到 202、
receiptId和duplicate: false,且没有error。 - 用同一投递 ID 和正文重试一次。应返回 200、同一个
receiptId和duplicate: true,不再产生新的事件。 - 请管理方在控制台的 入站记录 中按发送时间找到这次请求(也可以在能使用该 Webhook 的频道或群聊里请 ZAI 按
receiptId查询),确认正文完整、事件定义命中;再检查 事件记录 和 订阅 中的投递状态,最后在接收事件的频道或群聊里确认任务结果。
控制台的 发送测试事件 用于验证样例分类和投递,会跳过外部请求的验签;它不能代替发送系统的网络连通、凭证与签名联调。
如果请求已接收但没有任务,先检查事件名、动作和正文是否与事件定义一致,再检查事件定义与订阅是否启用、接收事件的频道或群聊是否仍能使用该 Webhook。反馈问题时提供 receiptId、投递 ID、发送时间和脱敏后的请求样例即可,不要附上 Token、签名密钥或含 Token 的完整地址。