> This page is for 플랫폼, version 레거시.
> For other versions, use one of these documentation indexes:
> - V4 (default): https://next.developer.frame.io/platform/v4/llms.txt
> - V4 실험적: https://next.developer.frame.io/platform/v4-experimental/llms.txt
> - 레거시: https://next.developer.frame.io/platform/v2/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://next.developer.frame.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://next.developer.frame.io/_mcp/server.

# 웹훅 개요

<Info title="애플리케이션 예시">
  Frame.io 웹훅을 위한 자체 컨슈머를 직접 구축하고 싶다면, [Github에 있는 예제 앱](https://github.com/Frameio/webhooks-example-app)을 다운로드하여 자유롭게 확장해 보세요.
</Info>


## 소개




웹훅은 Frame.io 내부에서 발생하는 이벤트를 외부 시스템으로 전송하여 처리, API 콜백, 및 궁극적으로는 워크플로 자동화를 위해 알림으로 활용할 수 있는 방법을 제공합니다.




## 설정

[웹훅은 저희 개발자 사이트](https://developer.frame.io/app/webhooks)의 Webhooks 영역에서 구성할 수 있습니다. 웹훅에는 다음 항목이 필요합니다.
* Name — 개발자 사이트에서만 표시됩니다.
* URL — 이벤트를 전달할 위치.
* Team — 이 웹훅이 추가될 팀.
* Events — 웹훅을 트리거할 하나 이상의 이벤트.




## 지원되는 이벤트




단일 웹훅은 다음 이벤트 중 원하는 개수만큼 구독할 수 있습니다.





### 프로젝트




| 이벤트 | 트리거 |
| ---------- | ---------- |
| `project.created` | 새 프로젝트가 생성되었습니다. |
| `project.updated` | 프로젝트 설정이 업데이트되었습니다. |
| `project.deleted` | 프로젝트가 삭제되었습니다. |




### 에셋



| 이벤트 | 트리거 |
| ---------- | ---------- |
| `asset.created` | 에셋이 Frame.io에 처음 추가/생성되었지만, 에셋 업로드가 완전히 끝나기 전일 수 있습니다. |
| `asset.copied` | 에셋이 복사되었습니다. |
| `asset.updated` | 에셋의 설명, 이름 또는 기타 파일 정보가 변경되었습니다. |
| `asset.deleted` | 에셋이 삭제되었습니다(수동 삭제 등). |
| `asset.ready` | 에셋이 업로드되고 처리된 후 모든 트랜스코딩이 완료되었습니다. |
| `asset.label.updated` | 에셋의 상태 레이블이 설정, 변경 또는 제거되었습니다. |
| `asset.versioned` | 에셋 버전이 생성되었습니다. |



<Warning title="에셋 버전 관리">
  `asset.versioned` 이벤트가 발생하면 버전 스택 자체가 아니라 버전이 지정된 에셋의 id가 포함된 페이로드를 받게 됩니다. 따라서 해당 `id`를 버전 스택의 id라고 생각하고 다른 함수에 전달하려고 한다면, 먼저 해당 특정 '상위' 리소스를 찾아 추적해야 합니다.
</Warning>

<Warning title="에셋 레이블 업데이트">
  공개 API를 사용하여 `/v2/assets/:id` 엔드포인트에 대한 `PUT` 호출을 통해 상태 레이블이 변경된 경우 `asset.label.updated` 이벤트가 발생하지 않습니다(`BES-408`). 하지만, 기본 Frame.io 앱 및 연동 프로그램(Web, iOS, Premiere, After Effects, FCPX 등)을 사용하여 상태 레이블을 업데이트할 때는 실행됩니다.
</Warning>


### 코멘트



| 이벤트 | 트리거 |
| ---------- | ---------- |
| `comment.created` | 새 코멘트 또는 답글이 생성되었습니다. |
| `comment.updated` | 코멘트가 편집되었습니다. |
| `comment.deleted` | 코멘트가 삭제되었습니다. |
| `comment.completed` | 코멘트가 완료 상태로 변경되었습니다. |
| `comment.uncompleted` | 코멘트가 미완료 상태로 변경되었습니다. |




### 검토 링크



| 이벤트 | 트리거 |
| ---------- | ---------- |
| `reviewlink.created` | 새 검토 링크가 생성되었습니다. |



### 공동 작업자




| 이벤트 | 트리거 |
| ---------- | ---------- |
| `collaborator.created` | 협업자가 계정에 추가되었습니다. |
| `collaborator.deleted` | 협업자가 계정에서 제거되었습니다. |




### 팀원



| 이벤트 | 트리거 |
| ---------- | ---------- |
| `teammember.created` | 팀 구성원이 계정에 추가되었습니다. |
| `teammember.deleted` | 팀 구성원이 계정에서 제거되었습니다. |




## 페이로드




Frame.io는 지정된 웹훅 엔드포인트로 JSON 페이로드를 전달합니다. 다음은 *asset.created* 이벤트에 대한 페이로드 예시입니다.





```json
{
  "type": "asset.created",
  "resource": {
    "type": "asset",
    "id": "<asset-id>"
  },
  "user": {
    "id": "<user-id>"
  },
  "team": {
    "id": "<team-id>"
  }
}
```

모든 페이로드에는 발생하는 이벤트의 유형을 나타내는 `type` 필드와 `resource` 오브젝트가 포함되어 있습니다. `resource` 오브젝트는 이 이벤트와 관련된 리소스의 `type` 및 `id`를 지정합니다. 위의 *asset.created* 이벤트 예시에서 이는 새로 생성된 에셋의 `id`가 됩니다. 또한 `user` 및 `team` 오브젝트도 포함됩니다. 여기에는 이벤트를 트리거한 사용자와 리소스의 팀 컨텍스트가 참조됩니다. 직접적인 User 및 Team 컨텍스트 외에, **구독된 리소스에 대한 추가 정보는 포함하지 않습니다**. 애플리케이션에 추가 정보나 컨텍스트가 필요한 경우, HTTP API를 사용하여 후속 요청을 수행하는 것을 권장합니다.

## 재시도




웹훅을 귀하의 서비스로 전달하는 동안 오류(200이 아닌 상태 코드 응답) 또는 시간 초과가 발생할 경우 페이로드 전송은 세 번 재시도되며, 총 4번의 전송 시도가 이루어집니다.




## 보안




기본적으로 모든 웹훅에는 서명 키가 제공됩니다. 이는 구성할 수 없습니다. 이 키를 사용하여 요청이 Frame.io에서 보낸 것인지 확인할 수 있습니다.




### 웹훅 서명 확인




중간자 공격 및 재전송 공격으로부터 연동 환경을 보호하려면 웹훅 서명을 확인하는 것이 필수적입니다. 이 확인 과정을 통해 웹훅 페이로드가 실제로 Frame.io에서 전송되었는지, 그리고 전송 중에 페이로드 콘텐츠가 수정되지 않았는지를 보장할 수 있습니다.

`POST` 요청에는 다음 헤더가 포함됩니다.
| 이름 | 설명 |
| ---------- | ---------- |
| `X-Frameio-Request-Timestamp` | 웹훅 전송 시간 |
| `X-Frameio-Signature` | 계산된 서명 |
**타임스탬프**는 Frame.io 시스템에서 전송된 시간입니다. 이 기능은 재전송 공격을 방지하는 데 사용할 수 있습니다. 이 시간이 현지 시간 기준 5분 이내인지 확인하는 것이 좋습니다. **서명**은 웹훅이 최초 생성될 때 제공된 서명 키를 사용하는 HMAC SHA256 해시입니다.

서명을 확인하려면 다음 단계를 따르세요.




1. HTTP 헤더에서 서명 추출
2. 버전, 전송 시간 및 요청 본문을 결합하여 서명할 메시지를 생성합니다. `v0:timestamp:body`
3. 서명 시크릿을 사용하여 HMAC SHA256 서명을 계산하세요. _참고: 제공된 서명은 `v0=` 접두사로 시작합니다. 현재 Frame.io에는 요청 서명을 위한 버전이 이 한 가지뿐입니다. 계산된 서명 앞에 이 접두사가 추가되었는지 확인하세요._
4. 비교하세요!





**`Python`**

```python title="Python"
import hmac
import hashlib

def verify_signature(curr_time, req_time, signature, body, secret):
    """
    Verify Webhook signature
    :Args:
        curr_time (float): Current epoch time
        req_time (float): Request epoch time
        signature (str): Signature provided by the Frame.io API for the given request
        body (str): Webhook body from the received POST
        secret (str): The secret for this Webhook that you saved when you first created it
    """
    if int(curr_time) - int(req_time) < 500:
        message = 'v0:{}:{}'.format(req_time, body)
        calculated_signature = 'v0={}'.format(hmac.new(
            bytes(secret, 'latin-1'),
            msg=bytes(message, 'latin-1'),
            digestmod=hashlib.sha256).hexdigest())
        if calculated_signature == signature:
            return True
    return False
```





```js
const crypto = require('crypto');

// Capture the signature, secret, timestamp and payload from a new webhook event:
const 
signature = 'v0=a77ce6856e609c884575c2fd211d07a9ad1c3f72e19c06ff710e8f086ffca883', 
secret = 'yxSE59T0gtZOFZxw6UhLwTkhd2m8ntNSdSWnApQ0xOnMEzSoXbD8sGFP4bzb7MbS',
timestamp = 1604004499,  // UNIX timestamp in seconds
payload = {
	"project": {
		"id": "f348e9f4-f142-42f9-b3bf-478d93f0feb4"
	},
	"resource": {
		"id": "6aad9151-c216-4d6f-b5e9-530df551a426",
		"type": "asset"
	},
	"team": {
		"id": "aa891687-4b1e-4150-9b6d-9e4911c5b436"
	},
	"type": "asset.label.updated",
	"user": {
		"id": "59c9ade1-311b-4c3b-8231-b9d88e9a1a85"
	}
},
body = JSON.stringify(payload),

// Validate that caught payload is not older than 5 minutes
currentTimeUTC = (new Date()).getTime(),
currentTimestamp = currentTimeUTC / 1000, // JavaScript uses milliseconds whereas Unix Time is in seconds.
minutes = 5, 
expired = (currentTimestamp - timestamp) > minutes*60
hmac1 = crypto.createHmac('sha256', secret),
generateSignature = hmac1.update(`v0:${timestamp}:${body}`).digest('hex')

// Evaluates to true if the webhook is verified
console.log(!expired && signature === `v0=${generateSignature}`)
```





```go
// Full Go sample code: https://github.com/Frameio/webhooks-example-app/blob/master/main.go

func handler(w http.ResponseWriter, r *http.Request) {
	out, err := httputil.DumpRequest(r, true)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	log.Println(string(out))

	// Verify the message has been delivered in the last 5 minutes.
	timestampStr := r.Header.Get("X-Frameio-Request-Timestamp")
	timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	if time.Since(time.Unix(timestamp, 0)) > 5*time.Minute {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	// Verify request signature.
	expected := r.Header.Get("X-Frameio-Signature")
	signature, _ := computeSignature(r, timestamp, secretKey)
	if expected != signature {
		w.WriteHeader(http.StatusUnauthorized)
		return
	}

	var event *Event
	decoder := json.NewDecoder(r.Body)
	err = decoder.Decode(&event)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	// Handle webhook here.
	log.Println(event.ID)

	w.WriteHeader(http.StatusOK)
}

// The request includes headers to enable the recipient to validate
// that the request is from Frame.io and that it's been delivered within
// the expected time range. To learn more about how this works, take a
// look at our docs https://docs.frame.io/docs/webhooks#section-security.
func computeSignature(r *http.Request, timestamp int64, secret string) (string, error) {
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		return "", err
	}
	copy := body[:]
	r.Body = ioutil.NopCloser(bytes.NewReader(copy))

	msg := fmt.Sprintf("%s:%d:%s", version, timestamp, string(body))

	key := []byte(secret)
	h := hmac.New(sha256.New, key)
	h.Write([]byte(msg))

	result := fmt.Sprintf("%s=%s", version, hex.EncodeToString(h.Sum(nil)))

	return result, nil
}
```