# Webhooks

> Recevez un webhook signé à la fin d'une transcription ou en cas d'échec, et vérifiez sa signature.

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

Pour être averti quand une transcription est prête ou a échoué, renseignez `webhook_url` lors de sa création. transcript1 enverra un `POST` signé à cette adresse.

Les webhooks sont réservés à l'API REST. Vous ne pouvez pas les configurer depuis le [serveur MCP](https://transcript1.com/fr/docs/mcp), qui ne propose pas ce paramètre.

## Indiquer où recevoir les webhooks

Avec un lien vidéo accepté par l'API ou lors de la réservation d'un import par blocs, ajoutez `webhook_url` au JSON envoyé à `POST /v1/transcriptions`. Si vous envoyez le fichier en une seule requête, ajoutez plutôt ce paramètre au formulaire :

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

Utilisez une URL HTTPS, sans nom d'utilisateur ni mot de passe. Elle doit mener à une adresse publique : les adresses privées, loopback, link-local et toute autre adresse non publique sont refusées avec `422 invalid_request`. L'API contrôle l'adresse à la création de la transcription, puis avant chaque envoi du webhook.

Vous ne pouvez pas utiliser `webhook_url` avec `reuse_recent=true`.

## Événements

| `type` | Quand le webhook est envoyé |
|---|---|
| `transcription.completed` | Le statut passe à `completed` : le texte est prêt. |
| `transcription.failed` | Le statut passe à `failed`. Vous trouverez le `code` et le `message` d'erreur dans `data.error`. |

Si vous annulez la transcription, aucun webhook n'est envoyé.

## Lire le contenu du webhook

Le webhook contient un corps 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
  }
}
```

Le champ `created_at` indique quand l'événement a été créé. Le bloc `data` reprend l'objet transcription, sans `transcript` ; l'exemple ci-dessus n'en montre qu'une partie. Pour récupérer le texte, appelez `GET /v1/transcriptions/:id` en utilisant l'`id` présent dans `data`.

Les en-têtes comprennent `Content-Type: application/json`, `User-Agent` avec la valeur `transcript1-webhooks/1.0`, et la signature décrite ci-dessous.

## Vérifier la signature

Vous trouverez la signature de chaque requête dans `X-Transcript1-Signature` :

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

- `t` donne l'instant de signature sous forme de timestamp Unix, en secondes.
- `v1` contient le HMAC-SHA256 en hexadécimal de la chaîne `"<t>.<raw body>"`. Pour la construire, prenez la valeur de `t`, ajoutez un point, puis le corps de la requête tel que vous l'avez reçu. Utilisez votre secret de signature complet comme clé, sans retirer le préfixe `whsec_`.

Calculez la signature à partir des octets bruts du corps, avant de parser le JSON. Si vous parsez puis sérialisez à nouveau le JSON, les espaces ou l'ordre des clés peuvent changer : la signature ne correspondra plus.

Comparez les signatures en temps constant. Vérifiez aussi l'écart entre `t` et l'heure actuelle, et refusez les requêtes trop éloignées dans le temps. Les exemples ci-dessous acceptent un écart de 5 minutes.

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

Avec Express, récupérez le corps en texte brut pour vérifier la signature avant de parser le 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)
```

Avec Flask, récupérez le corps avec `request.get_data(as_text=True)` et transmettez-le dans `raw_body`.

## Afficher ou renouveler le secret de signature

Le secret commence par `whsec_`. Il est commun à votre équipe et sert à signer les webhooks de toutes ses transcriptions. Pour l'afficher ou le renouveler, ouvrez la page [« Webhooks »](https://transcript1.com/dashboard/webhooks) du tableau de bord, à l'adresse `/dashboard/webhooks`.

Le renouvellement prend effet immédiatement. Tous les webhooks envoyés ensuite utilisent le nouveau secret.

## Délais, nouvelles tentatives et doublons

- Répondez avec un statut `2xx` pour confirmer la réception.
- Tout autre statut, une erreur de connexion ou l'absence de réponse sous 10 secondes fait échouer la tentative.
- Après un échec, transcript1 réessaie, dans la limite de 6 tentatives au total. Le délai double à chaque fois : 2, 4, 8, 16, puis 32 minutes. La dernière tentative a donc lieu environ une heure après la première.
- Donnez directement l'URL finale : transcript1 ne suit pas les redirections. Un statut `3xx` compte comme un échec.
- Prévoyez les doublons. Un webhook peut arriver plusieurs fois, notamment si votre serveur répond après le délai d'attente. Appuyez-vous sur `data.id` et `type` pour ne pas traiter deux fois le même événement.
