Webhook

Webhook 是一种配置,当 Space 中发生某些事件时(例如 Content 的创建、发布)自动执行预先设定的动作。动作二选一:向外部 URL 发送 HTTP 请求(url),或运行 Space 内的 Scriptscript)。用于外部系统集成或自动化。例如,可以配置为每当商品 Content 被发布时调用公司内部的通知服务器,或使用预先设定的 Script 运行后续任务。

urlscript 只能指定其中一个。同时指定两者或两者都留空都会被拒绝。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.idWebhook 的唯一标识符。用于单条查询、修改、删除路径中的 {webhookId}
  • url:事件发生时要调用的外部目标 URL。与 script 只能指定其中一个。
  • script:用于代替外部调用而运行的 Script 引用。与 url 只能指定其中一个。上面的示例中没有该字段。详见下方 url 与 script(二选一)
  • runAsscript 以哪个用户身份运行。详见下方 runAs
  • topics:用于指定订阅哪些事件的数组。格式详见下方 topics
  • filters:在已订阅的事件中,实际触发的条件。详见下方 filters
  • transformation:用于改变发往 url 的请求形态(方法、请求体等)的配置。详见下方 transformation

系统属性 (sys)

每个 Webhook 都在 sys 对象中保存公共的系统属性。spacecreatedByupdatedByRefer 形态({ "sys": { "id", "type": "Refer", "targetType" } })呈现。

属性类型说明
idstring资源的唯一标识符。
typestring资源种类。Webhook 始终为 "Webhook"
spaceRefer<Space>Webhook 所属的 Space
createdByRefer<User>创建该资源的用户。
createdAtstring (date-time)创建时间。
updatedByRefer<User>最后修改该资源的用户。
updatedAtstring (date-time)最后修改时间。
versioninteger (≥1)资源版本。每次修改递增 1。

Webhook 是配置资源,因此没有发布这一概念。与 ContentContent Type 不同,它不具有 publisharchivestatus 等发布状态属性,只有用于跟踪变更的 version。开启和关闭不是通过发布,而是通过请求体字段 activate 来控制。

请求体属性

Webhook 的请求体(创建、修改时发送,并在响应中返回的配置值)由以下字段组成。

字段类型必填说明
namestring (1~64)Webhook 名称。
urlstring (url)事件发生时要调用的外部目标 URL。与 script 二选一。参见下方 url 与 script(二选一)
scriptRefer<Script>用于代替外部调用而运行的 Script 引用。与 url 二选一。参见下方 url 与 script(二选一)
runAsWebhookRunAsscript 运行时的用户身份。HookOwner(默认)或 EventUser。参见下方 runAs
activateboolean是否开启。为 false 时即使事件发生也不会执行。
topicsstring[]要订阅的事件数组。参见下方 topics
filtersFilter[]触发条件数组。留空则已订阅的所有事件都会触发。参见下方 filters
headersWebhookHeader[] (0~30)url 调用一同发送的 HTTP 头数组。
httpBasicUsernamestring (1~32)url 调用的 HTTP Basic 认证用户名。
httpBasicPasswordstring (1~32)url 调用的 HTTP Basic 认证密码。仅可写入,不会出现在响应中。
transformationTransformation对发往 url 的请求进行自定义。参见下方 transformation

标记为 △ 的 urlscript 只能指定其中一个。同时指定两者或两者都留空都会被拒绝。

headers 的每一项由 key(必填)、value(必填)、secret(可选,boolean)组成。将 secret 设为 true 时,该值会在发送记录中以被遮蔽的形式留下(参见下方 WebhookLog)。但查询这个 Webhook 时,该值会以原文返回。 从响应中省略的只有 httpBasicPassword,因此请把放在 secret 头中的值视为:凡是能读取这个 Webhook 的角色都能看到,并把该角色的范围收窄。

topics

topics 的每一项采用 {资源}.{动作} 格式。例如:Content.CreateContent.PublishMedia.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.idsys.contentType.sys.idsys.createdBy.sys.idsys.updatedBy.sys.id 之一。
  • op:比较运算符。为 EQNEINNOT_INREGEXNOT_REGEX 之一。
  • value:比较值。EQNEREGEXNOT_REGEX 使用字符串,INNOT_IN 使用字符串数组。

设置多个过滤条件时,需全部满足才会触发(AND)。filters 留空时,已订阅 topics 的所有事件都会触发。

transformation

transformation 改变发往 url 的 HTTP 请求的形态(对使用 scriptWebhook 不适用)。未指定时,整个资源载荷以默认的 POST 原样发送。

类型说明
methodstringHTTP 方法。为 GETPOSTPUTDELETEPATCH 之一。
contentTypestring请求体的 Content-Type。请求体按该格式序列化(见下文)。
bodyobject用 JSON Pointer 模板构建待发送请求体的对象。
includeBodyboolean是否一并发送触发资源的请求体。

请求体以何种格式发出

contentType 决定请求体的序列化格式。比较时忽略大小写以及 ;charset=… 之类的参数,只看前面的部分。未指定或值为空时,按 application/json 发送。includeBodyfalsemethodGET 时不发送请求体,此时也不会附加 Content-Type

Webhook 发送的请求体始终是对象。因为 body 模板是对象,而未设置模板时,被触发的资源整体会原样发出。

声明的 contentType实际发出的 Content-Type发出的请求体
(无)application/jsonJSON
application/json与声明值相同JSON
application/x-www-form-urlencoded与声明值相同product[sku]=TUMBLER-500&product[price]=24000
text/plainapplication/jsonJSON
其他(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 }"
  }
}

skuTUMBLER-500price24000Content 触发时,请求体以 sku=TUMBLER-500&price=24000 的形式发出。

url 与 script(二选一)

Webhook 被触发时执行二者之一。指定 url 时,向该外部 URL 发送 HTTP 请求(请求形态由 transformationheadershttpBasic* 决定)。指定 script 时,不向外部发送,而是运行 Space 内的一个 Script

  • url:外部目标 URL(http/https)。私有网络、回环地址等被阻止的目标会被拒绝。
  • script:要运行的 ScriptRefer

二者只能指定其中一个。同时指定两者或两者都留空时会被拒绝,且返回的错误码按路径而异(参见错误)。

"script": { "sys": { "id": "3trmXRMZcTAjDnphewjj1AaxYcaxlK", "type": "Refer", "targetType": "Script" } }

被触发时,Script 以委托的权限运行,各 statement 的资源权限不会在运行时重新检查。允许执行什么,在编写 Script 时就已检查。详细的执行与权限模型请参见 Script 的执行语义、约束与安全

runAs

runAs 决定 script 以哪个用户身份运行。该身份会成为运行期间创建或修改的资源的 createdBy/updatedByScriptcreatedBy: ":self" 过滤条件也按该身份解析。它只是归属(attribution),并非权限边界。 能做什么由编写 Script 时的权限检查决定。

运行身份
HookOwner创建该 Webhook 的用户(sys.createdBy)。默认值。
EventUser引发该事件(变更)的用户,即被触发资源的 sys.updatedBy

在只使用 urlWebhook 中,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 指向。它不是用户,而是该 WebhookRefersys.updatedBy 也是同一个 Webhook

属性类型说明
idstring记录的唯一标识符。
typestring始终为 "WebhookLog"
spaceRefer<Space>该记录所属的 Space
requestIdstring本次发送尝试的追踪标识符。
statusCodeinteger收到的响应的 HTTP 状态码。
errorsstring[]失败原因列表。在向 URL 发送的 Webhook 的记录中始终为空(由状态码说明失败)。只有用 script 运行 ScriptWebhook 的记录中,才会载入该 Script 的失败消息。
eventTypestring引发本次发送的动作名称(例如 CreatePublish)。载入的不是写在 topics 中的 Content.Create 那种形式,而只有后半的动作。
urlstring发送目标 URL。
requestAtstring (date-time)发送请求的时间。
responseAtstring (date-time)收到响应的时间。
requestobject发送的请求。其下级结构见下文。列表查询中会省略。
responseobject收到的响应。其下级结构见下文。列表查询中会省略。
createdByRefer<Webhook>留下该记录的 Webhook
createdAtstring (date-time)记录创建时间。
updatedByRefer<Webhook>createdBy 相同。
updatedAtstring (date-time)createdAt 相同。

requestresponse 各自拥有以下键。

  • requesturl(发送请求的目标 URL) · method(HTTP 方法) · headers(发送的头映射) · body(发送的正文字符串)。
  • responseurl(收到响应的 URL) · headers(收到的头映射) · body(收到的正文字符串) · statusCode(收到的状态码)。

script 运行 ScriptWebhook,其记录的形态不同。由于没有要发送的地址,所以没有 urlrequestmethod 固定为 "SCRIPT"requestbody 中载入引发本次发送的 payload,responsebody 中载入该 Script 返回的值(或失败消息)。

开启了 secret 的头,其值会被遮蔽后保存。实际值不会留在记录中。

过长的正文会缩短后保存。 requestbody 以 65,536 个字符、responsebody 以 8,192 个字符为基准。超过这一长度时,会保留前后、省略中间,并在该位置写上被省略的字符数。若正文是 JSON,为了不破坏结构,只会以相同方式缩短较长的字符串值,因此键和较短的值会原样保留。

区分成功与失败的标准因集成方式而异。 向 URL 发送的 Webhook,响应为 2xx 或 3xx 即为成功。用 script 运行 ScriptWebhook,则在 statusCode 小于 400 且 errors 为空时才算成功。这一个判定同时决定下面的保存期限和发送状态的成功率。

列表查询会去掉 requestresponse 后返回。 这是因为列表端点的 select 默认值为 -sys.response,-sys.request。若要连发送的请求和收到的响应的正文一起查看,请使用单条查询,或直接指定 select 覆盖该默认值。

成功发送的记录在 1 小时后消失,失败发送的记录在 3 天后消失。响应中没有承载过期时间的字段,时间一到记录就会自行消失。需要保存更久的值,请在接收方服务器上另行保存,或在用 script 运行的 Script 中留存为 Content

错误

以下是处理 Webhook 时会遇到的错误码。所有资源共通的错误码,请参见通用错误

错误码条件
WGL400042创建(POST)与整体修改(PUT)的请求同时指定了 urlscript,或两者都留空。
WGL422061部分修改(PATCH)的请求同时指定了 urlscript,或两者都留空。
WGL422050url 指向私有网络、回环地址等被阻止的目标。

API

以下所有端点的基准 URL 为 https://cma.weegloo.com/v1Authorization 头中需要用于认证 CMA 的 Bearer 令牌。修改(PUT)与部分修改(PATCH)为实现乐观并发控制,需一并发送 X-Weegloo-Version 头(当前资源的 sys.version)。

  • Content:触发 Webhook 的主体数据。
  • Media:可以触发 Webhook 的文件资源。
  • Script:用 script 运行的声明式后端端点。包含执行与权限模型。
  • SpaceRole:承载 Script 执行(Execute)权限等的角色配置。