> ## Documentation Index
> Fetch the complete documentation index at: https://api-docs.voicecheap.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Recevez un rappel HTTP dès qu'un projet atteint une étape clé, au lieu d'utiliser le polling

# Webhooks

Les webhooks permettent à VoiceCheap de notifier votre serveur dès qu'un événement se produit, afin que vous puissiez arrêter le polling
`GET /v1/translate/{projectId}/status`.

<Note>
  Les livraisons sont tentées **une fois**. Il n'y a pas encore de tentatives de réessai, donc continuez à utiliser le polling comme filet de sécurité pour
  tout ce que vous ne pouvez pas vous permettre de manquer. Des réessais avec backoff exponentiel sont prévus.
</Note>

## Configuration

1. Ouvrez la [page API](https://voicecheap.ai/page-api) dans votre compte VoiceCheap.
2. Dans **Webhooks**, sélectionnez **Générer le secret de signature**. Le secret commence par `whsec_` et est affiché
   **une fois** — copiez-le et stockez-le sur votre serveur.
3. Entrez votre **URL de point de terminaison** et enregistrez-la. Elle doit utiliser `https`.

C'est tout. Chaque projet que vous démarrez à partir de ce moment enverra des événements à ce point de terminaison.

### Remplacement du point de terminaison par requête

`POST /v1/translate` et `POST /v1/projects` acceptent un champ optionnel `webhookUrl` qui remplace le
point de terminaison du compte pour ce projet uniquement. C'est pratique pour envoyer le trafic de staging ailleurs :

```bash theme={null}
curl -X POST "https://api.voicecheap.ai/v1/projects" \
  -H "x-api-key: vc_your-key" \
  -F "file=@interview.mp4" \
  -F "targetLanguage=german" \
  -F "webhookUrl=https://staging.your-server.com/voicecheap/webhooks"
```

Le secret de signature est toujours le secret du compte ; seule la destination change.

## Événements

| Événement                       | Se déclenche quand                                                                         |
| ------------------------------- | ------------------------------------------------------------------------------------------ |
| `project.created`               | Le projet existe et sa transcription est stockée. C'est le signal pour télécharger le SRT. |
| `project.creation.failed`       | Le projet n'a pas pu être créé.                                                            |
| `project.translation.completed` | La traduction est terminée et les sorties sont prêtes.                                     |
| `project.translation.failed`    | La traduction a échoué.                                                                    |
| `project.lipsync.completed`     | La synchronisation labiale est terminée.                                                   |
| `project.lipsync.failed`        | La synchronisation labiale a échoué.                                                       |

<Note>
  Il n'y a pas d'événement de transcription distinct. La transcription s'exécute lors de la création du projet, et un projet
  n'existe qu'une fois sa transcription stockée — donc `project.created` signifie déjà que la transcription est
  prête à être récupérée avec [`POST /v1/projects/{projectId}/transcript`](/docs/fr/api-reference/project-transcript).
</Note>

## Charge utile

Chaque livraison est un `POST` avec un corps JSON :

```json theme={null}
{
  "type": "project.created",
  "eventId": "evt_9f2c4b1d8e7a4c3f9b2d1e0a5c6b7d8e",
  "projectId": "7fa7d3a3-4f2b-4c1e-9a6d-2b3c4d5e6f70",
  "workflow": "transcription",
  "status": "success",
  "targetLanguage": "german",
  "error": null,
  "occurredAt": "2026-08-19T09:12:00.000Z"
}
```

| Champ            | Type           | Description                                                                                                                       |
| ---------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `type`           | string         | L'un des événements ci-dessus.                                                                                                    |
| `eventId`        | string         | Unique par événement. Utilisez-le pour dédoublonner.                                                                              |
| `projectId`      | string         | Le projet auquel cet événement appartient.                                                                                        |
| `workflow`       | string         | `transcription` ou `translation`.                                                                                                 |
| `status`         | string         | `success` ou `failed`.                                                                                                            |
| `targetLanguage` | string         | Présent uniquement sur les événements spécifiques à une langue.                                                                   |
| `error`          | object \| null | Sur les événements `*.failed`, la même forme `{ code, message }` que l'API REST. Voir [Codes d'erreur](/docs/fr/api-reference/errors). |
| `occurredAt`     | string         | Horodatage ISO 8601.                                                                                                              |

La charge utile est volontairement légère. Appelez l'API REST pour le contenu lui-même.

## Vérification de la signature

Chaque livraison comporte ces en-têtes :

| En-tête                  | Description                                                                                  |
| ------------------------ | -------------------------------------------------------------------------------------------- |
| `X-VoiceCheap-Signature` | `sha256=<hex>` — HMAC-SHA256 de la charge utile, indexé par votre secret de signature.       |
| `X-VoiceCheap-Timestamp` | Secondes Unix, incluses dans la chaîne signée afin que vous puissiez rejeter les relectures. |
| `X-VoiceCheap-Event-Id`  | Même valeur que `eventId` dans le corps, utile pour les demandes d'assistance.               |

La signature couvre `<timestamp>.<raw body>`, elle est donc différente à chaque livraison et prouve à la fois
que la requête provient de VoiceCheap et que le corps n'a pas été modifié en transit.

```js theme={null}
import crypto from 'crypto';
import express from 'express';

const app = express();

// The raw body is required: parsing and re-serializing changes the bytes and the signature will
// never match. This is the single most common cause of failed verification.
app.post('/voicecheap/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  const rawBody = req.body.toString('utf8');
  const timestamp = req.header('X-VoiceCheap-Timestamp');
  const signature = req.header('X-VoiceCheap-Signature')?.replace('sha256=', '') ?? '';

  // Reject anything older than five minutes.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return res.status(400).send('Stale timestamp');
  }

  const expected = crypto
    .createHmac('sha256', process.env.VOICECHEAP_WEBHOOK_SECRET)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');

  const isValid =
    expected.length === signature.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));

  if (!isValid) return res.status(401).send('Invalid signature');

  const event = JSON.parse(rawBody);
  // Acknowledge quickly, then process asynchronously.
  res.status(200).send('ok');
  handleEvent(event);
});
```

<Warning>
  Vérifiez la signature avant de faire confiance à une livraison. Votre point de terminaison est une URL publique, et la signature est
  ce qui distingue un véritable événement VoiceCheap de tout autre élément qui l'atteint.
</Warning>

## Réponse

Répondez avec n'importe quel statut `2xx` pour accuser réception. Répondez dans les **10 secondes** — accusez réception d'abord et effectuez
le travail ensuite, plutôt que de traiter avant de répondre.

Une réponse autre que 2xx ou un délai d'attente est enregistré comme une livraison échouée et, pour l'instant, n'est pas réessayé.

## Rotation du secret

Sélectionnez **Rotation** sur la page API pour générer un nouveau secret. Le précédent cesse de fonctionner
immédiatement, alors déployez le nouveau secret sur votre serveur dès que vous effectuez la rotation.

La suppression du webhook supprime à la fois le point de terminaison et le secret, et les livraisons s'arrêtent.
