Webhook の概要

サンプルアプリケーション

Frame.io Webhook 用に独自のコンシューマーを構築したい場合は、GitHub 上のサンプルアプリを入手して拡張してください。

利用開始

Webhook は、Frame.io 内で発生するイベントを活用して、外部システムに処理、API コールバック、そして最終的にはワークフローの自動化用に送信できる通知に変換する方法を提供します。

セットアップ

Webhook は開発者サイトの webhook エリアで設定できます。Webhook には以下が必要です。

  • 名前 - 開発者サイトにのみ表示されます。
  • URL - イベントの配信先。
  • チーム - この Webhook を追加するチーム。
  • イベント - Webhook をトリガーするイベント。

サポートされているイベント

1 つの 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 とみなして別の関数に渡せると考えている場合は、まずその特定の ‘parent’ リソースを検索して突き止める必要があります。

アセットラベルの更新

パブリック API(BES-408)を介して /v2/assets/:id エンドポイントへの PUT 呼び出しによってステータスラベルが変更されても、asset.label.updated イベントは発生しません。ただし、ステータスラベルの更新にネイティブの 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 は、JSON ペイロードを、指定された Webhook エンドポイントに配信します。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 になります。さらに、user および team オブジェクトも含まれます。これらは、イベントをトリガーしたユーザーと、リソースのチームコンテキストを参照します。

ユーザーとチームの直接的なコンテキストを除き、サブスクライブされたリソースに関する追加情報は含まれません。アプリケーションで追加情報やコンテキストが必要な場合、HTTP API を使用してフォローアップリクエストを行うことをお勧めします。

再試行

サービスへの Webhook の配信中にエラー(200 以外のステータスコード応答)またはタイムアウトが発生した場合、ペイロードが 3 回再試行され、合計 4 回の配信試行が行われます。

セキュリティ

デフォルトでは、すべての Webhook に署名キーが提供されます。これは設定できません。このキーを使用して、リクエストが 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}