# Receba avisos por webhook

> Configure um webhook para saber quando a transcrição terminou ou falhou. Veja como conferir a assinatura de cada aviso.

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

Quer saber quando a transcrição terminou sem consultar o status? Informe `webhook_url` na criação. O transcript1 envia um `POST` assinado para esse endereço quando a transcrição fica pronta ou falha.

Esse recurso está disponível só na API REST. As ferramentas do [servidor MCP](https://transcript1.com/pt/docs/mcp) não aceitam um parâmetro de webhook.

## Configure o endereço de entrega

Na chamada a `POST /v1/transcriptions`, coloque `webhook_url` no corpo JSON se estiver enviando um link aceito ou criando um upload em partes. Se enviar o arquivo de uma vez, use um campo de formulário com esse nome:

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

Use uma URL HTTPS, sem usuário ou senha no endereço. Ela precisa apontar para um IP público. Endereços privados, de loopback, link-local ou qualquer outro endereço não público geram `422 invalid_request`. Conferimos o endereço ao criar a transcrição e antes de cada entrega.

Não envie `webhook_url` junto com `reuse_recent=true`.

## Eventos

| `type` | Quando você recebe |
|---|---|
| `transcription.completed` | O status muda para `completed`. |
| `transcription.failed` | O status muda para `failed`. Confira `code` e `message` em `data.error`. |

Se você cancelar a transcrição, não enviamos webhook.

## Payload

O aviso chega com este corpo 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
  }
}
```

O horário de criação do evento vem em `created_at`. Em `data`, você recebe o objeto da transcrição, mas sem o campo `transcript`; o exemplo acima está abreviado. Para buscar o texto, use o `id` de `data` em `GET /v1/transcriptions/:id`.

Também enviamos `Content-Type: application/json`, `User-Agent` com o valor `transcript1-webhooks/1.0` e o cabeçalho de assinatura abaixo.

## Confira a assinatura antes de ler o JSON

Você encontra a assinatura de cada entrega no cabeçalho `X-Transcript1-Signature`:

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

- `t` informa quando assinamos a requisição, em segundos Unix.
- `v1` contém o HMAC-SHA256 em hexadecimal da string `"<t>.<raw body>"`. Monte essa string com o valor de `t`, um ponto e o corpo exatamente como chegou. Use o segredo de assinatura inteiro como chave, com o prefixo `whsec_`.

Calcule a assinatura sobre os bytes originais do corpo, antes de fazer o parse do JSON. Se você converter o JSON em objeto e depois em texto, os espaços ou a ordem das chaves podem mudar e a assinatura deixa de conferir. Faça a comparação em tempo constante e recuse requisições com `t` muito distante da hora atual. Nos exemplos, a tolerância é de 5 minutos.

### 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));
};
```

No Express, receba o corpo como texto, sem passar pelo parser de JSON antes da verificação:

```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)
```

No Flask, obtenha o corpo com `request.get_data(as_text=True)` e passe esse texto em `raw_body`.

## Consulte ou troque o segredo de assinatura

Cada equipe tem um segredo de assinatura com o prefixo `whsec_`, usado em todas as suas transcrições. Para consultar ou gerar outro, abra [“Webhooks”](https://transcript1.com/dashboard/webhooks), em `/dashboard/webhooks`. A troca vale imediatamente: a partir daí, todas as entregas usam o novo segredo.

## Entregas e novas tentativas

- Responda com qualquer status `2xx` para confirmar o recebimento.
- Outro status, erro de conexão ou falta de resposta em 10 segundos faz a tentativa falhar.
- Se a entrega falhar, tentamos de novo com backoff exponencial, até 6 tentativas no total. Os intervalos são de 2, 4, 8, 16 e 32 minutos. A última tentativa ocorre cerca de uma hora após a primeira.
- Informe a URL final. O transcript1 não segue redirecionamentos e considera respostas `3xx` uma falha de entrega.
- Seu servidor pode receber o mesmo aviso mais de uma vez, por exemplo se responder depois do timeout. Confira `data.id` e `type` para não processar duplicatas.
