Webhook
Webhook 是一种配置,当 Space 中发生某些事件时(例如 Content 的创建、发布)自动执行预先设定的动作。动作二选一:向外部 URL 发送 HTTP 请求(url),或运行 Space 内的 Script(script)。用于外部系统集成或自动化。例如,可以配置为每当商品 Content 被发布时调用公司内部的通知服务器,或使用预先设定的 Script 运行后续任务。
url 和 script 只能指定其中一个。同时指定两者或两者都留空都会被拒绝。Webhook 在 CMA 中是 Space 的下级资源,路径以 /spaces/{spaceId}/webhooks 为基准。
资源结构
以下是 Webhook「商品变更通知」的单条查询响应。除 sys(系统属性)外,它还包含发送目标、订阅事件、触发条件等配置字段。
{
"sys": {
"id": "3trmXRM3RqbgSnifyg7PWhk01Examp",
"type": "Webhook",
"space": { "sys": { "id": "HnQ32YiH", "type": "Refer", "targetType": "Space" } },
"createdBy": { "sys": { "id": "3p4tcFbQRwz503VXdtHXNI5dZH5TVB", "type": "Refer", "targetType": "User" } },
"createdAt": "2026-06-18T11:30:00.000Z",
"updatedBy": { "sys": { "id": "3p4tcFbQRwz503VXdtHXNI5dZH5TVB", "type": "Refer", "targetType": "User" } },
"updatedAt": "2026-06-18T11:30:00.000Z",
"version": 1
},
"name": "商品变更通知",
"filters": [
{ "doc": "sys.contentType.sys.id", "op": "EQ", "value": "3trmXRLdJF4GBlAjtcuoZ7Pnxj8dlA" }
],
"headers": [
{ "key": "X-Source", "value": "weegloo", "secret": false }
],
"httpBasicUsername": "dailywear",
"topics": ["Content.Create", "Content.Publish"],
"transformation": { "method": "POST", "contentType": "application/json", "includeBody": true },
"url": "https://api.dailywear.example/webhooks/products",
"activate": true,
"runAs": "HookOwner"
}主要键:
sys.id:Webhook 的唯一标识符。用于单条查询、修改、删除路径中的{webhookId}。url:事件发生时要调用的外部目标 URL。与script只能指定其中一个。script:用于代替外部调用而运行的 Script 引用。与url只能指定其中一个。上面的示例中没有该字段。详见下方 url 与 script(二选一)。runAs:script以哪个用户身份运行。详见下方 runAs。topics:用于指定订阅哪些事件的数组。格式详见下方 topics。filters:在已订阅的事件中,实际触发的条件。详见下方 filters。transformation:用于改变发往url的请求形态(方法、请求体等)的配置。详见下方 transformation。
系统属性 (sys)
每个 Webhook 都在 sys 对象中保存公共的系统属性。space、createdBy、updatedBy 以 Refer 形态({ "sys": { "id", "type": "Refer", "targetType" } })呈现。
| 属性 | 类型 | 说明 |
|---|---|---|
id | string | 资源的唯一标识符。 |
type | string | 资源种类。Webhook 始终为 "Webhook"。 |
space | Refer<Space> | 此 Webhook 所属的 Space。 |
createdBy | Refer<User> | 创建该资源的用户。 |
createdAt | string (date-time) | 创建时间。 |
updatedBy | Refer<User> | 最后修改该资源的用户。 |
updatedAt | string (date-time) | 最后修改时间。 |
version | integer (≥1) | 资源版本。每次修改递增 1。 |
Webhook 是配置资源,因此没有发布这一概念。与 Content 或 Content Type 不同,它不具有 publish、archive、status 等发布状态属性,只有用于跟踪变更的 version。开启和关闭不是通过发布,而是通过请求体字段 activate 来控制。
请求体属性
Webhook 的请求体(创建、修改时发送,并在响应中返回的配置值)由以下字段组成。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string (1~64) | ✅ | Webhook 名称。 |
url | string (url) | △ | 事件发生时要调用的外部目标 URL。与 script 二选一。参见下方 url 与 script(二选一)。 |
script | Refer<Script> | △ | 用于代替外部调用而运行的 Script 引用。与 url 二选一。参见下方 url 与 script(二选一)。 |
runAs | WebhookRunAs | script 运行时的用户身份。HookOwner(默认)或 EventUser。参见下方 runAs。 | |
activate | boolean | ✅ | 是否开启。为 false 时即使事件发生也不会执行。 |
topics | string[] | ✅ | 要订阅的事件数组。参见下方 topics。 |
filters | Filter[] | ✅ | 触发条件数组。留空则已订阅的所有事件都会触发。参见下方 filters。 |
headers | WebhookHeader[] (0~30) | ✅ | 随 url 调用一同发送的 HTTP 头数组。 |
httpBasicUsername | string (1~32) | url 调用的 HTTP Basic 认证用户名。 | |
httpBasicPassword | string (1~32) | url 调用的 HTTP Basic 认证密码。仅可写入,不会出现在响应中。 | |
transformation | Transformation | ✅ | 对发往 url 的请求进行自定义。参见下方 transformation。 |
标记为 △ 的 url、script 只能指定其中一个。同时指定两者或两者都留空都会被拒绝。
headers 的每一项由 key(必填)、value(必填)、secret(可选,boolean)组成。将 secret 设为 true 时,该值会在发送记录中以被遮蔽的形式留下(参见下方 WebhookLog)。但查询这个 Webhook 时,该值会以原文返回。 从响应中省略的只有 httpBasicPassword,因此请把放在 secret 头中的值视为:凡是能读取这个 Webhook 的角色都能看到,并把该角色的范围收窄。
topics
topics 的每一项采用 {资源}.{动作} 格式。例如:Content.Create、Content.Publish、Media.Create。
动作是以下之一,或表示该资源所有动作的 *(例如:Content.*)。
| 动作 | 含义 |
|---|---|
All | 所有动作。 |
Create | 创建。 |
Read | 查询。 |
Edit | 编辑。 |
Save | 保存(修改)。修改事件是 Save,不是 Update。 |
Delete | 删除。 |
Publish | 发布。 |
Unpublish | 取消发布。 |
Archive | 归档。 |
Unarchive | 取消归档。 |
filters
filters 是一个数组,用于在已订阅的 topics 中缩小实际触发 Webhook 的条件。每个过滤条件的形态如下。
{ "doc": "sys.contentType.sys.id", "op": "EQ", "value": "3trmXRLdJF4GBlAjtcuoZ7Pnxj8dlA" }doc:要比较的字段路径。为sys.id、sys.contentType.sys.id、sys.createdBy.sys.id、sys.updatedBy.sys.id之一。op:比较运算符。为EQ、NE、IN、NOT_IN、REGEX、NOT_REGEX之一。value:比较值。EQ、NE、REGEX、NOT_REGEX使用字符串,IN、NOT_IN使用字符串数组。
设置多个过滤条件时,需全部满足才会触发(AND)。filters 留空时,已订阅 topics 的所有事件都会触发。
transformation
transformation 改变发往 url 的 HTTP 请求的形态(对使用 script 的 Webhook 不适用)。未指定时,整个资源载荷以默认的 POST 原样发送。
| 键 | 类型 | 说明 |
|---|---|---|
method | string | HTTP 方法。为 GET、POST、PUT、DELETE、PATCH 之一。 |
contentType | string | 请求体的 Content-Type。请求体按该格式序列化(见下文)。 |
body | object | 用 JSON Pointer 模板构建待发送请求体的对象。 |
includeBody | boolean | 是否一并发送触发资源的请求体。 |
请求体以何种格式发出
contentType 决定请求体的序列化格式。比较时忽略大小写以及 ;charset=… 之类的参数,只看前面的部分。未指定或值为空时,按 application/json 发送。includeBody 为 false 或 method 为 GET 时不发送请求体,此时也不会附加 Content-Type。
Webhook 发送的请求体始终是对象。因为 body 模板是对象,而未设置模板时,被触发的资源整体会原样发出。
声明的 contentType | 实际发出的 Content-Type | 发出的请求体 |
|---|---|---|
| (无) | application/json | JSON |
application/json | 与声明值相同 | JSON |
application/x-www-form-urlencoded | 与声明值相同 | product[sku]=TUMBLER-500&product[price]=24000 |
text/plain | application/json | JSON |
其他(text/xml 等) | 与声明值相同 | JSON |
text/plain 无法承载对象,因此会更正为可以承载对象的格式后发送。不会出现请求头与实际请求体说法不一致的情况。如果目标必须以文本形式接收请求体,仅靠 contentType 无法解决,请确认接收方的约定。
form-urlencoded 将对象展开为方括号键,将数组展开为索引。
| 请求体 | 展开后的键与值 |
|---|---|
{ "product": { "sku": "TUMBLER-500", "price": 24000 } } | product[sku]=TUMBLER-500&product[price]=24000 |
{ "tags": ["kitchen", "insulated"] } | tags[0]=kitchen&tags[1]=insulated |
{ "items": [{ "sku": "TUMBLER-500" }] } | items[0][sku]=TUMBLER-500 |
{ "memo": null } | memo= |
键与值以 UTF-8 进行百分号编码后发出。上表为了展示键的结构,采用了解码后的形态。即使值中包含 & 或 +,也不会被误认为键值对分隔符或空格,而是原样传递。
将嵌套展开为方括号键的表示法是广泛采用的惯例,并非该格式本身的规范。 请确认接收方是否会把 product[sku] 还原为嵌套对象,如果不会还原,请用扁平的键构建 body 模板。
以下是以表单形式发送的 transformation 示例。
"transformation": {
"method": "POST",
"contentType": "application/x-www-form-urlencoded",
"includeBody": true,
"body": {
"sku": "{ /payload/fields/sku/ko-KR }",
"price": "{ /payload/fields/price/ko-KR }"
}
}sku 为 TUMBLER-500、price 为 24000 的 Content 触发时,请求体以 sku=TUMBLER-500&price=24000 的形式发出。
url 与 script(二选一)
Webhook 被触发时执行二者之一。指定 url 时,向该外部 URL 发送 HTTP 请求(请求形态由 transformation、headers、httpBasic* 决定)。指定 script 时,不向外部发送,而是运行 Space 内的一个 Script。
url:外部目标 URL(http/https)。私有网络、回环地址等被阻止的目标会被拒绝。script:要运行的 Script 的Refer。
二者只能指定其中一个。同时指定两者或两者都留空时会被拒绝,且返回的错误码按路径而异(参见错误)。
"script": { "sys": { "id": "3trmXRMZcTAjDnphewjj1AaxYcaxlK", "type": "Refer", "targetType": "Script" } }被触发时,Script 以委托的权限运行,各 statement 的资源权限不会在运行时重新检查。允许执行什么,在编写 Script 时就已检查。详细的执行与权限模型请参见 Script 的执行语义、约束与安全。
runAs
runAs 决定 script 以哪个用户身份运行。该身份会成为运行期间创建或修改的资源的 createdBy/updatedBy,Script 内 createdBy: ":self" 过滤条件也按该身份解析。它只是归属(attribution),并非权限边界。 能做什么由编写 Script 时的权限检查决定。
| 值 | 运行身份 |
|---|---|
HookOwner | 创建该 Webhook 的用户(sys.createdBy)。默认值。 |
EventUser | 引发该事件(变更)的用户,即被触发资源的 sys.updatedBy。 |
在只使用 url 的 Webhook 中,runAs 会被忽略。未指定时为 HookOwner。
WebhookLog
Webhook 每尝试发送一次,就会留下一条记录。它只能查询,没有创建、修改、删除端点。路径为 /spaces/{spaceId}/webhooks/{webhookId}/logs。
{
"sys": {
"id": "3trmXRM3RqbgSnifyg7PWhc01Exam",
"type": "WebhookLog",
"space": { "sys": { "id": "HnQ32YiH", "type": "Refer", "targetType": "Space" } },
"requestId": "3trmXRM3qWnLb7Vd1yPcYs04kKrjq",
"statusCode": 200,
"errors": [],
"eventType": "Create",
"url": "https://api.dailywear.example/webhooks/products",
"requestAt": "2026-06-18T11:35:00.100Z",
"responseAt": "2026-06-18T11:35:00.350Z",
"request": {
"url": "https://api.dailywear.example/webhooks/products",
"method": "POST",
"headers": { "Content-Type": "application/json", "X-Source": "weegloo" },
"body": "{\"sys\":{\"type\":\"Content\"}}"
},
"response": {
"url": "https://api.dailywear.example/webhooks/products",
"headers": { "Content-Type": "application/json" },
"body": "{\"ok\":true}",
"statusCode": 200
},
"createdBy": { "sys": { "id": "3trmXRM3RqbgSnifyg7PWhk01Examp", "type": "Refer", "targetType": "Webhook" } },
"createdAt": "2026-06-18T11:35:00.350Z",
"updatedBy": { "sys": { "id": "3trmXRM3RqbgSnifyg7PWhk01Examp", "type": "Refer", "targetType": "Webhook" } },
"updatedAt": "2026-06-18T11:35:00.350Z"
}
}所有值都在 sys 中,没有正文属性。没有值的键会从响应中省略。
是哪个 Webhook 留下的记录,由 sys.createdBy 指向。它不是用户,而是该 Webhook 的 Refer,sys.updatedBy 也是同一个 Webhook。
| 属性 | 类型 | 说明 |
|---|---|---|
id | string | 记录的唯一标识符。 |
type | string | 始终为 "WebhookLog"。 |
space | Refer<Space> | 该记录所属的 Space。 |
requestId | string | 本次发送尝试的追踪标识符。 |
statusCode | integer | 收到的响应的 HTTP 状态码。 |
errors | string[] | 失败原因列表。在向 URL 发送的 Webhook 的记录中始终为空(由状态码说明失败)。只有用 script 运行 Script 的 Webhook 的记录中,才会载入该 Script 的失败消息。 |
eventType | string | 引发本次发送的动作名称(例如 Create、Publish)。载入的不是写在 topics 中的 Content.Create 那种形式,而只有后半的动作。 |
url | string | 发送目标 URL。 |
requestAt | string (date-time) | 发送请求的时间。 |
responseAt | string (date-time) | 收到响应的时间。 |
request | object | 发送的请求。其下级结构见下文。列表查询中会省略。 |
response | object | 收到的响应。其下级结构见下文。列表查询中会省略。 |
createdBy | Refer<Webhook> | 留下该记录的 Webhook。 |
createdAt | string (date-time) | 记录创建时间。 |
updatedBy | Refer<Webhook> | 与 createdBy 相同。 |
updatedAt | string (date-time) | 与 createdAt 相同。 |
request 与 response 各自拥有以下键。
request:url(发送请求的目标 URL) ·method(HTTP 方法) ·headers(发送的头映射) ·body(发送的正文字符串)。response:url(收到响应的 URL) ·headers(收到的头映射) ·body(收到的正文字符串) ·statusCode(收到的状态码)。
用 script 运行 Script 的 Webhook,其记录的形态不同。由于没有要发送的地址,所以没有 url,request 的 method 固定为 "SCRIPT"。request 的 body 中载入引发本次发送的 payload,response 的 body 中载入该 Script 返回的值(或失败消息)。
开启了 secret 的头,其值会被遮蔽后保存。实际值不会留在记录中。
过长的正文会缩短后保存。 request 的 body 以 65,536 个字符、response 的 body 以 8,192 个字符为基准。超过这一长度时,会保留前后、省略中间,并在该位置写上被省略的字符数。若正文是 JSON,为了不破坏结构,只会以相同方式缩短较长的字符串值,因此键和较短的值会原样保留。
区分成功与失败的标准因集成方式而异。 向 URL 发送的 Webhook,响应为 2xx 或 3xx 即为成功。用 script 运行 Script 的 Webhook,则在 statusCode 小于 400 且 errors 为空时才算成功。这一个判定同时决定下面的保存期限和发送状态的成功率。
列表查询会去掉 request 与 response 后返回。 这是因为列表端点的 select 默认值为 -sys.response,-sys.request。若要连发送的请求和收到的响应的正文一起查看,请使用单条查询,或直接指定 select 覆盖该默认值。
成功发送的记录在 1 小时后消失,失败发送的记录在 3 天后消失。响应中没有承载过期时间的字段,时间一到记录就会自行消失。需要保存更久的值,请在接收方服务器上另行保存,或在用 script 运行的 Script 中留存为 Content。
错误
以下是处理 Webhook 时会遇到的错误码。所有资源共通的错误码,请参见通用错误。
| 错误码 | 条件 |
|---|---|
WGL400042 | 创建(POST)与整体修改(PUT)的请求同时指定了 url 和 script,或两者都留空。 |
WGL422061 | 部分修改(PATCH)的请求同时指定了 url 和 script,或两者都留空。 |
WGL422050 | url 指向私有网络、回环地址等被阻止的目标。 |
API
以下所有端点的基准 URL 为 https://cma.weegloo.com/v1,Authorization 头中需要用于认证 CMA 的 Bearer 令牌。修改(PUT)与部分修改(PATCH)为实现乐观并发控制,需一并发送 X-Weegloo-Version 头(当前资源的 sys.version)。
