Webhook 概述

示例应用程序

如果您想为 Frame.io Webhook 构建自己的消费者,请随时获取并扩展我们在 GitHub 上的示例应用程序

前言

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

设置

Webhook 可以在我们开发者网站的 Webhook 区域进行配置。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版本化资产时
资产版本化

asset.versioned 事件触发时,您将收到一个包含已版本化资产 ID 的有效载荷,而不是版本堆栈本身。因此,如果您期望能够将该 id 传递给另一个函数,并认为它是版本堆栈的 id,那么您必须先查找并追踪那个特定的“主”资源。

资产标签更新

当通过公共 API 对 /v2/assets/:id 端点发起 PUT 调用来更改状态标签时,asset.label.updated 事件不会触发 (BES-408)。但是,当使用任何原生 Frame.io 应用程序和集成(Web、iOS、Premiere、After Effects、FCPX 等)更新状态标签时,该事件会触发。

评论

事件触发时机
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 事件的负载示例:

1{
2 "type": "asset.created",
3 "resource": {
4 "type": "asset",
5 "id": "<asset-id>"
6 },
7 "user": {
8 "id": "<user-id>"
9 },
10 "team": {
11 "id": "<team-id>"
12 }
13}

所有负载都包含一个 type 字段,用于指示发生的事件类型,以及一个 resource 对象。resource 对象指定了与此事件相关的资源的 typeid。在上述 asset.created 事件示例中,这将是新创建资产的 id。此外,负载还包含 userteam 对象。它们分别引用了触发该事件的用户,以及该资源所属的团队上下文。除了直接的用户和团队上下文之外,我们不会包含有关订阅资源的任何其他信息。如果您的应用程序需要其他信息或上下文,我们建议使用我们的 HTTP API 发起后续请求。

重试

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

安全性

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

验证 Webhook 签名

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

POST 请求中包含以下标头:

名称描述
X-Frameio-Request-TimestampWebhook 传递的时间
X-Frameio-Signature计算得出的签名
时间戳是来自 Frame.io 系统的传递时间。这可用于防止重放攻击。我们建议验证此时间与本地时间相差是否在 5 分钟以内。签名是使用首次创建 Webhook 时提供的签名密钥的 HMAC SHA256 哈希值。

请按照以下步骤验证签名:

  1. 从 HTTP 标头中提取签名
  2. 通过组合版本、传递时间和请求体:v0:timestamp:body 来创建一个用于签名的消息
  3. 使用您的签名密钥计算 HMAC SHA256 签名。注意:提供的签名以 v0= 为前缀。目前 Frame.io 只有这一种用于请求签名的版本。请确保在您计算出的签名前也加上此前缀。
  4. 比较!
Python
1import hmac
2import hashlib
3
4def verify_signature(curr_time, req_time, signature, body, secret):
5 """
6 Verify Webhook signature
7 :Args:
8 curr_time (float): Current epoch time
9 req_time (float): Request epoch time
10 signature (str): Signature provided by the Frame.io API for the given request
11 body (str): Webhook body from the received POST
12 secret (str): The secret for this Webhook that you saved when you first created it
13 """
14 if int(curr_time) - int(req_time) < 500:
15 message = 'v0:{}:{}'.format(req_time, body)
16 calculated_signature = 'v0={}'.format(hmac.new(
17 bytes(secret, 'latin-1'),
18 msg=bytes(message, 'latin-1'),
19 digestmod=hashlib.sha256).hexdigest())
20 if calculated_signature == signature:
21 return True
22 return False
1const crypto = require('crypto');
2
3// Capture the signature, secret, timestamp and payload from a new webhook event:
4const
5signature = 'v0=a77ce6856e609c884575c2fd211d07a9ad1c3f72e19c06ff710e8f086ffca883',
6secret = 'yxSE59T0gtZOFZxw6UhLwTkhd2m8ntNSdSWnApQ0xOnMEzSoXbD8sGFP4bzb7MbS',
7timestamp = 1604004499, // UNIX timestamp in seconds
8payload = {
9 "project": {
10 "id": "f348e9f4-f142-42f9-b3bf-478d93f0feb4"
11 },
12 "resource": {
13 "id": "6aad9151-c216-4d6f-b5e9-530df551a426",
14 "type": "asset"
15 },
16 "team": {
17 "id": "aa891687-4b1e-4150-9b6d-9e4911c5b436"
18 },
19 "type": "asset.label.updated",
20 "user": {
21 "id": "59c9ade1-311b-4c3b-8231-b9d88e9a1a85"
22 }
23},
24body = JSON.stringify(payload),
25
26// Validate that caught payload is not older than 5 minutes
27currentTimeUTC = (new Date()).getTime(),
28currentTimestamp = currentTimeUTC / 1000, // JavaScript uses milliseconds whereas Unix Time is in seconds.
29minutes = 5,
30expired = (currentTimestamp - timestamp) > minutes*60
31hmac1 = crypto.createHmac('sha256', secret),
32generateSignature = hmac1.update(`v0:${timestamp}:${body}`).digest('hex')
33
34// Evaluates to true if the webhook is verified
35console.log(!expired && signature === `v0=${generateSignature}`)
1// Full Go sample code: https://github.com/Frameio/webhooks-example-app/blob/master/main.go
2
3func handler(w http.ResponseWriter, r *http.Request) {
4 out, err := httputil.DumpRequest(r, true)
5 if err != nil {
6 w.WriteHeader(http.StatusInternalServerError)
7 return
8 }
9
10 log.Println(string(out))
11
12 // Verify the message has been delivered in the last 5 minutes.
13 timestampStr := r.Header.Get("X-Frameio-Request-Timestamp")
14 timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
15 if err != nil {
16 w.WriteHeader(http.StatusBadRequest)
17 return
18 }
19
20 if time.Since(time.Unix(timestamp, 0)) > 5*time.Minute {
21 w.WriteHeader(http.StatusBadRequest)
22 return
23 }
24
25 // Verify request signature.
26 expected := r.Header.Get("X-Frameio-Signature")
27 signature, _ := computeSignature(r, timestamp, secretKey)
28 if expected != signature {
29 w.WriteHeader(http.StatusUnauthorized)
30 return
31 }
32
33 var event *Event
34 decoder := json.NewDecoder(r.Body)
35 err = decoder.Decode(&event)
36 if err != nil {
37 w.WriteHeader(http.StatusInternalServerError)
38 return
39 }
40
41 // Handle webhook here.
42 log.Println(event.ID)
43
44 w.WriteHeader(http.StatusOK)
45}
46
47// The request includes headers to enable the recipient to validate
48// that the request is from Frame.io and that it's been delivered within
49// the expected time range. To learn more about how this works, take a
50// look at our docs https://docs.frame.io/docs/webhooks#section-security.
51func computeSignature(r *http.Request, timestamp int64, secret string) (string, error) {
52 body, err := ioutil.ReadAll(r.Body)
53 if err != nil {
54 return "", err
55 }
56 copy := body[:]
57 r.Body = ioutil.NopCloser(bytes.NewReader(copy))
58
59 msg := fmt.Sprintf("%s:%d:%s", version, timestamp, string(body))
60
61 key := []byte(secret)
62 h := hmac.New(sha256.New, key)
63 h.Write([]byte(msg))
64
65 result := fmt.Sprintf("%s=%s", version, hex.EncodeToString(h.Sum(nil)))
66
67 return result, nil
68}