> 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.

# Webhook 概述

<Info title="示例应用程序">
  如果您想为 Frame.io Webhook 构建自己的消费者，请随时获取并扩展我们[在 GitHub 上的示例应用程序](https://github.com/Frameio/webhooks-example-app)。
</Info>


## 前言




Webhook 提供了一种方式，可以将 Frame.io 内部发生的事件转化为通知，这些通知可以发送到外部系统进行处理、作为 API 回调，并最终实现工作流自动化。




## 设置

Webhook 可以在[我们开发者网站的 Webhook 区域](https://developer.frame.io/app/webhooks)进行配置。Webhook 需要以下信息：
* 名称 — 仅在开发者网站上显示。
* URL — 事件的传送目标位置。
* 团队 — 此 Webhook 将添加到哪个团队。
* 事件 — 哪个或哪些事件应该触发该 Webhook。




## 支持的事件




单个 Webhook 可以订阅以下任意数量的事件：





### 项目




| 事件 | 触发时机 |
| ---------- | ---------- |
| `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 会向指定的 Webhook 端点传递一个 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` 对象。它们分别引用了触发该事件的用户，以及该资源所属的团队上下文。除了直接的用户和团队上下文之外，**我们不会包含有关订阅资源的任何其他信息**。如果您的应用程序需要其他信息或上下文，我们建议使用我们的 HTTP API 发起后续请求。

## 重试




如果在向您的服务传递 Webhook 时发生错误（非 200 状态代码响应）或超时，负载将被重试三次，总共会进行四次传递尝试。




## 安全性




默认情况下，所有 Webhoo 都会提供一个签名密钥。此项不可配置。此密钥可用于验证请求是否来自 Frame.io。




### 验证 Webhook 签名




为了保护集成免受中间人攻击和重放攻击，验证 Webhook 签名至关重要。验证可确保 Webhook 负载确实由 Frame.io 发送，并且负载内容在传输过程中未被修改。

`POST` 请求中包含以下标头：
| 名称 | 描述 |
| ---------- | ---------- |
| `X-Frameio-Request-Timestamp` | Webhook 传递的时间 |
| `X-Frameio-Signature` | 计算得出的签名 |
**时间戳**是来自 Frame.io 系统的传递时间。这可用于防止重放攻击。我们建议验证此时间与本地时间相差是否在 5 分钟以内。**签名**是使用首次创建 Webhook 时提供的签名密钥的 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
}
```