# Вебхуки

> Настройте уведомления о готовом транскрипте и ошибках. Проверяйте подпись входящих запросов и обрабатывайте повторную доставку.

Canonical: https://transcript1.com/ru/docs/webhooks
Language: ru
Last updated: 2026-10-14

Чтобы не опрашивать статус, укажите `webhook_url` при создании задачи. Когда транскрибация завершится успешно или с ошибкой, transcript1 отправит на этот адрес подписанный `POST`.

Настроить вебхук можно только через REST API. Инструменты [MCP-сервера](https://transcript1.com/ru/docs/mcp) не принимают этот параметр.

## Куда отправлять уведомления

При отправке ссылки на видео или создании загрузки по частям добавьте `webhook_url` в JSON для `POST /v1/transcriptions`. Если загружаете файл одним запросом, передайте его полем формы:

```json
{
  "platform": "youtube",
  "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "webhook_url": "https://example.com/hooks/transcript1"
}
```

Укажите HTTPS-ссылку без логина и пароля. Она должна разрешаться в публичный адрес: частные, loopback, link-local и другие непубличные адреса дадут `422 invalid_request`. Проверяем адрес и при создании задачи, и перед каждой отправкой вебхука.

В одном запросе нельзя передавать `webhook_url` и `reuse_recent=true`.

## События

| `type` | Что произошло |
|---|---|
| `transcription.completed` | Транскрипт готов, статус задачи — `completed`. |
| `transcription.failed` | Задача завершилась с `failed`. Код и описание ошибки находятся в `data.error`, в полях `code` и `message`. |

Если вы отменили транскрибацию, уведомления не будет.

## Тело запроса

Уведомление приходит в формате JSON:

```json
{
  "type": "transcription.completed",
  "created_at": "2026-10-13T09:30:04.000Z",
  "data": {
    "id": "tr_8fK2Lm0PqRsTuVwXyZaBcD",
    "object": "transcription",
    "status": "completed",
    "transcript_source": "youtube_captions",
    "billed_minutes": 4,
    "error": null
  }
}
```

В `created_at` указано время события, а в `data` — объект транскрибации без самого текста, то есть без `transcript`. Выше приведён сокращённый пример. Возьмите `id` из `data` и получите транскрипт запросом `GET /v1/transcriptions/:id`.

В заголовках будут `Content-Type: application/json`, `User-Agent: transcript1-webhooks/1.0` и подпись. Ниже — как её проверить.

## Как проверить подпись

С каждым вебхуком передаём заголовок `X-Transcript1-Signature`:

```text
X-Transcript1-Signature: t=<unix seconds>,v1=<hex digest>
```

- `t` — Unix-время подписания запроса, в секундах.
- `v1` — шестнадцатеричный HMAC-SHA256 для строки `"<t>.<raw body>"`. Соберите её из `t`, точки и тела запроса без каких-либо изменений. В качестве ключа возьмите секрет подписи целиком, с префиксом `whsec_`.

Проверяйте подпись по исходным байтам тела, до разбора JSON. После разбора и обратной сериализации могут измениться пробелы или порядок ключей — подпись уже не совпадёт. Сравнение должно занимать постоянное время. Проверяйте и `t`: слишком большое расхождение с текущим временем — повод отклонить запрос. В примерах ниже допустимое расхождение составляет 5 минут.

### Node.js

```javascript
import { Buffer } from 'node:buffer';
import crypto from 'node:crypto';

// header: X-Transcript1-Signature, rawBody: the request body exactly as received,
// secret: your full signing secret including the whsec_ prefix.
export const verifySignature = (secret, header, rawBody, nowSeconds = Math.floor(Date.now() / 1000)) => {
  const fields = Object.fromEntries(header.split(',').map(part => part.trim().split('=')));
  const t = Number(fields.t);

  if (!Number.isInteger(t) || !fields.v1 || Math.abs(nowSeconds - t) > 300) {
    return false;
  }

  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');

  return fields.v1.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(fields.v1), Buffer.from(expected));
};
```

В Express сначала прочитайте тело как текст, не подключая разбор JSON:

```javascript
import process from 'node:process';
import express from 'express';
import { verifySignature } from './verify-signature.js';

const app = express();

app.post('/hooks/transcript1', express.text({ type: 'application/json' }), (request, response) => {
  const header = request.get('X-Transcript1-Signature') ?? '';

  if (!verifySignature(process.env.TRANSCRIPT1_WEBHOOK_SECRET, header, request.body)) {
    response.sendStatus(400);
    return;
  }

  const { type, data } = JSON.parse(request.body);
  console.log(type, data.id, data.status);
  response.sendStatus(200);
});
```

### Python

```python
import hashlib
import hmac
import time


def verify_signature(secret, header, raw_body, now=None):
    # header: the X-Transcript1-Signature header, raw_body: the request body exactly as received,
    # secret: your full signing secret including the whsec_ prefix.
    fields = dict(part.strip().split("=", 1) for part in header.split(",") if "=" in part)
    try:
        t = int(fields["t"])
    except (KeyError, ValueError):
        return False

    now = int(time.time()) if now is None else now
    if abs(now - t) > 300:
        return False

    expected = hmac.new(secret.encode(), f"{t}.{raw_body}".encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(fields.get("v1", ""), expected)
```

В Flask получите исходное тело через `request.get_data(as_text=True)` и передайте его в `raw_body`.

## Где взять секрет подписи

У команды один секрет для вебхуков всех её транскрибаций. Он начинается с `whsec_`; посмотреть или заменить его можно на странице [«Webhooks»](https://transcript1.com/dashboard/webhooks) (`/dashboard/webhooks`). После замены старый секрет сразу перестаёт действовать. Все последующие вебхуки подписываем новым.

## Доставка и повторные попытки

- Верните любой код `2xx`, чтобы подтвердить получение вебхука.
- Другой статус, ошибка соединения или отсутствие ответа за 10 секунд означают, что попытка не удалась.
- При неудаче повторяем отправку с растущими паузами: 2, 4, 8, 16 и 32 минуты. Всего будет не больше 6 попыток, последняя — примерно через час после первой.
- Сразу указывайте конечный URL. transcript1 не следует редиректам: ответ `3xx` тоже считается неудачей.
- Один вебхук может прийти несколько раз — например, если ваш сервер ответил уже после таймаута. Отсеивайте дубликаты по сочетанию `data.id` и `type`.
