> 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
> - Версия 4 экспериментальная: 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). Для веб-перехватчика требуется:
* Название — будет отображаться только на сайте для разработчиков.
* URL-адрес — место доставки событий.
* Команда — к какой команде будет добавлен этот веб-перехватчик.
* События — какое событие или какие события должны запускать веб-перехватчик.




## Поддерживаемые события




Один веб-перехватчик может подписаться на любое количество следующих событий:





### Проекты




| Событие | Триггер |
| ---------- | ---------- |
| `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`, вы получите полезную нагрузку с идентификатором ресурса, который был версионирован, а не сам стек версий. Поэтому, чтобы передать этот `идентификатор` в другую функцию, полагая, что это идентификатор стека версий, вам сначала потребуется найти и отследить этот конкретный «родительский» ресурс.
</Warning>

<Warning title="Обновления меток ресурса">
  Событие `asset.label.updated` не сработает, когда метка статуса изменится через вызов `PUT` к конечной точке `/v2/assets/:id` через общедоступный API-интерфейс (`BES-408`). Однако оно сработает, когда метка статуса обновится с помощью любых встроенных приложений и интеграций Frame.io (веб-версия, 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>"
  }
}
```

Все полезные нагрузки содержат поле `тип`, указывающее тип происходящего события, а также объект `resource`. Объект `resource` определяет `тип` и `ИД` ресурса, связанного с этим событием. В приведенном выше примере события *asset.created* это будет `ИД` для только что созданного ресурса. Дополнительно включены объекты `пользователь` и `команда`. Они ссылаются на пользователя, который инициировал событие, и контекст команды для ресурса. Помимо непосредственного контекста пользователя и команды, **мы не включаем никакой дополнительной информации о подписанном ресурсе**. Если вашему приложению требуется дополнительная информация или контекст, рекомендуется использовать наш API-интерфейс HTTP для выполнения последующих запросов.

## Повторные попытки




В случае ошибки (ответ с кодом состояния, отличным от 200) или таймаута при доставке веб-перехватчика вашему сервису отправка полезной нагрузки будет повторяться три раза, всего четыре попытки.




## Безопасность




По умолчанию все веб-перехватчики предоставляются с ключом подписи. Это не настраивается. Данный ключ можно использовать для проверки того, исходит ли запрос от 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
}
```