Webhook

WebhookSpace에서 일이 일어났을 때(예: Content 생성·발행) 정해 둔 동작을 자동으로 실행하는 설정입니다. 동작은 둘 중 하나입니다. 외부 URL로 HTTP 요청을 보내거나(url), Space 안의 Script를 실행합니다(script). 외부 시스템 연동이나 자동화에 씁니다. 예를 들어 상품 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.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·updatedByRefer 모양({ "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과 달리 publish·archive·status 같은 발행 상태 속성을 가지지 않고, 변경 추적용 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 인증 비밀번호. 쓰기 전용입니다. 응답에는 나오지 않습니다.
transformationTransformationurl로 나가는 요청 커스터마이즈. 아래 transformation 참조.

△ 표시한 url·script정확히 하나만 지정합니다. 둘 다 지정하거나 둘 다 비우면 거부됩니다.

headers의 각 항목은 key(필수)·value(필수)·secret(선택, boolean)로 이루어집니다. secrettrue로 두면 그 값이 전송 기록에 가려진 채 남습니다(아래 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

transformationurl로 나가는 HTTP 요청의 모양을 바꿉니다(script를 쓰는 Webhook에는 적용되지 않습니다). 지정하지 않으면 리소스 페이로드 전체가 기본 POST로 그대로 나갑니다.

타입설명
methodstringHTTP 메서드. GET·POST·PUT·DELETE·PATCH 중 하나.
contentTypestring요청 본문의 Content-Type. 본문이 이 형식으로 직렬화됩니다(아래).
bodyobject보낼 본문을 JSON Pointer 템플릿으로 구성하는 객체.
includeBodyboolean트리거 리소스의 본문을 함께 보낼지 여부.

본문이 어떤 형식으로 나가는지

contentType이 본문의 직렬화 형식을 정합니다. 비교는 대소문자와 ;charset=… 같은 파라미터를 무시하고 앞부분만 봅니다. 지정하지 않거나 값이 비어 있으면 application/json으로 보냅니다. includeBodyfalse이거나 methodGET이면 본문을 보내지 않고, 그때는 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-500, price24000Content가 트리거하면 본문은 sku=TUMBLER-500&price=24000으로 나갑니다.

url과 script (택일)

Webhook이 트리거되면 둘 중 하나를 수행합니다. url을 지정하면 그 외부 URL로 HTTP 요청을 보냅니다(요청 모양은 transformation·headers·httpBasic*로 정합니다). script를 지정하면 외부로 나가지 않고 Space 안의 Script 하나를 실행합니다.

  • url: 외부 대상 URL(http/https). 사설망·루프백 등 차단된 대상은 거부됩니다.
  • script: 실행할 ScriptRefer.

둘은 정확히 하나만 지정해야 합니다. 둘 다 지정하거나 둘 다 비우면 거부되며, 돌아오는 코드는 경로마다 다릅니다(오류 참조).

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

트리거되면 Script는 위임된 권한으로 실행되며, statement별 리소스 권한은 실행 시 재검사하지 않습니다. 무엇이 허용되는지는 Script를 저작할 때 이미 검사됩니다. 자세한 실행·권한 모델은 Script의 실행 시맨틱, 제약, 보안을 참조하세요.

runAs

runAsscript어떤 사용자 신원으로 실행되는지를 정합니다. 이 신원은 실행 중 만들어지거나 수정되는 리소스의 createdBy/updatedBy가 되고, ScriptcreatedBy: ":self" 필터도 이 신원 기준으로 풀립니다. 귀속(attribution)일 뿐 권한 경계가 아닙니다. 무엇을 할 수 있는지는 Script를 저작할 때의 권한 검사로 정해집니다.

실행 신원
HookOwnerWebhook을 만든 사용자(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가 가리킵니다. 사용자가 아니라 그 WebhookRefer이며, sys.updatedBy도 같은 Webhook입니다.

속성타입설명
idstring기록의 고유 식별자.
typestring항상 "WebhookLog".
spaceRefer<Space>이 기록이 속한 Space.
requestIdstring이 전송 시도의 추적 식별자.
statusCodeinteger받은 응답의 HTTP 상태 코드.
errorsstring[]실패 사유 목록. URL로 보내는 Webhook의 기록에서는 항상 비어 있습니다(상태 코드가 실패를 말합니다). scriptScript를 실행하는 Webhook의 기록에서만 그 Script의 실패 메시지가 담깁니다.
eventTypestring이 전송을 일으킨 액션 이름입니다(예: Create·Publish). 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는 각각 다음 키를 가집니다.

  • request: url(요청을 보낸 대상 URL) · method(HTTP 메서드) · headers(보낸 헤더 맵) · body(보낸 본문 문자열).
  • response: url(응답을 받은 URL) · headers(받은 헤더 맵) · body(받은 본문 문자열) · statusCode(받은 상태 코드).

scriptScript를 실행하는 Webhook의 기록은 모양이 다릅니다. 보낼 주소가 없으므로 url이 없고, requestmethod"SCRIPT"로 고정됩니다. requestbody에는 그 전송을 일으킨 payload가, responsebody에는 그 Script가 돌려준 값(또는 실패 메시지)이 담깁니다.

secret을 켠 헤더의 값은 가려져서 저장됩니다. 실제 값은 기록에 남지 않습니다.

긴 본문은 줄여서 저장됩니다. requestbody는 65,536자, responsebody는 8,192자가 기준입니다. 그보다 길면 앞뒤를 남기고 가운데를 생략하며, 생략된 글자 수를 그 자리에 적어 둡니다. 본문이 JSON이면 구조를 깨지 않도록 긴 문자열 값만 같은 방식으로 줄이므로, 키와 짧은 값은 그대로 남습니다.

성공과 실패를 가르는 기준은 연동 방식마다 다릅니다. URL로 보내는 Webhook은 응답이 2xx 또는 3xx면 성공입니다. scriptScript를 실행하는 WebhookstatusCode가 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/v1이며, Authorization 헤더에 CMA를 인증하는 Bearer 토큰이 필요합니다. 수정(PUT)과 부분 수정(PATCH)은 낙관적 동시성 제어를 위해 X-Weegloo-Version 헤더(현재 리소스의 sys.version)를 함께 보내야 합니다.

  • Content: Webhook을 트리거하는 본문 데이터.
  • Media: Webhook을 트리거할 수 있는 파일 리소스.
  • Script: script로 실행할 선언형 백엔드 엔드포인트. 실행·권한 모델 포함.
  • SpaceRole: Script 실행(Execute) 권한 등을 담는 역할 설정.