# Ricevere e verificare i webhook

> Ricevi una notifica firmata quando il testo è pronto o la trascrizione non riesce. Ecco come verificarne la firma.

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

Vuoi evitare di controllare lo stato a intervalli? Specifica `webhook_url` alla creazione: transcript1 invia un `POST` firmato quando il testo è pronto o la trascrizione non riesce.

Puoi configurare i webhook solo nell'API REST. Il [server MCP](https://transcript1.com/it/docs/mcp) non accetta un parametro per attivarli.

## Scegliere l'URL che riceve le notifiche

In `POST /v1/transcriptions`, inserisci `webhook_url` nel body JSON se invii un link video supportato o apri un caricamento in parti. Se carichi tutto il file in una chiamata, aggiungilo ai campi del form:

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

Usa un URL HTTPS senza nome utente né password, che risolva a un indirizzo pubblico. Con indirizzi privati, di loopback, link-local o comunque non pubblici, ricevi `422 invalid_request`. Verifichiamo l'indirizzo sia alla creazione della trascrizione sia a ogni invio del webhook.

Non puoi usare `webhook_url` insieme a `reuse_recent=true`.

## Quali eventi ricevi

| `type` | Quando lo inviamo |
|---|---|
| `transcription.completed` | Lo stato passa a `completed`. |
| `transcription.failed` | Lo stato passa a `failed`. Trovi `code` e `message` in `data.error`. |

Se annulli una trascrizione, non inviamo webhook.

## Cosa contiene la notifica

Ricevi un body JSON con questa struttura:

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

In `created_at` trovi la data di creazione dell'evento. `data` contiene l'oggetto trascrizione, ma non il campo `transcript`; l'esempio sopra è abbreviato. Per ottenere il testo, usa l'`id` presente in `data` e chiama `GET /v1/transcriptions/:id`.

La chiamata ha `Content-Type: application/json` e `User-Agent` uguale a `transcript1-webhooks/1.0`. Include anche l'header per verificare la firma, descritto qui sotto.

## Controllare la firma prima di usare i dati

Leggi l'header `X-Transcript1-Signature`, presente a ogni invio:

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

- `t` indica il momento della firma, come timestamp Unix in secondi.
- Per ottenere `v1`, calcola l'HMAC-SHA256 della stringa `"<t>.<raw body>"` e codificalo in esadecimale. Unisci il valore di `t`, un punto e il body esattamente come lo hai ricevuto. Come chiave usa tutto il segreto di firma, incluso `whsec_`.

Controlla la firma sui byte originali del body, prima del parsing JSON. Se fai il parsing e poi serializzi di nuovo, potresti cambiare spazi o ordine delle chiavi: la firma non corrisponderebbe più. Usa un confronto in tempo costante e scarta le chiamate con un `t` troppo distante dall'ora corrente. Negli esempi la tolleranza è di 5 minuti.

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

In Express, acquisisci il body come testo prima di fare il parsing:

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

In Flask, usa `request.get_data(as_text=True)` per fornire il valore di `raw_body`.

## Gestire il segreto di firma

Ogni team ha un segreto con prefisso `whsec_`, usato per firmare i webhook di tutte le sue trascrizioni. Puoi visualizzarlo o sostituirlo nella pagina [“Webhooks”](https://transcript1.com/dashboard/webhooks), all'indirizzo `/dashboard/webhooks`. La sostituzione è immediata: da quel momento firmiamo i webhook con il nuovo segreto.

## Risposte, nuovi tentativi e duplicati

- Rispondi con un codice `2xx` per confermare la ricezione.
- Consideriamo il tentativo non riuscito se riceviamo un altro codice, incontriamo un errore di connessione o non otteniamo risposta entro 10 secondi.
- In quel caso riproviamo, fino a 6 tentativi totali, con backoff esponenziale: aspettiamo 2, 4, 8, 16 e 32 minuti tra un invio e l'altro. L'ultimo arriva circa un'ora dopo il primo.
- Indica direttamente l'URL finale: transcript1 non segue i redirect e considera una risposta `3xx` un tentativo non riuscito.
- Lo stesso evento può arrivare più volte, ad esempio se il tuo server risponde dopo il timeout. Riconosci i duplicati dalla coppia `data.id` e `type` e ignorali.
