> ## 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

> Ontvang een HTTP-callback zodra een project een mijlpaal bereikt, in plaats van te pollen

# Webhooks

Met webhooks kan VoiceCheap je server op de hoogte stellen zodra er iets gebeurt, zodat je kunt stoppen met pollen
`GET /v1/translate/{projectId}/status`.

<Note>
  Afleveringen worden **één keer** geprobeerd. Er zijn nog geen nieuwe pogingen, dus blijf pollen als vangnet voor
  alles wat je niet mag missen. Nieuwe pogingen met exponentiële uitstel zijn gepland.
</Note>

## Instellen

1. Open de [API-pagina](https://voicecheap.ai/page-api) in je VoiceCheap-account.
2. Selecteer bij **Webhooks** de optie **Generate signing secret**. Het geheim begint met `whsec_` en wordt
   **één keer** getoond — kopieer het en sla het op je server op.
3. Voer je **endpoint URL** in en sla deze op. Deze moet `https` gebruiken.

Dat is alles. Elk project dat je vanaf dat moment start, levert gebeurtenissen aan dat eindpunt.

### Het eindpunt per verzoek overschrijven

`POST /v1/translate` en `POST /v1/projects` accepteren een optioneel `webhookUrl`-veld dat het
accounteindpunt alleen voor dat project overschrijft. Dit is handig om staging-verkeer ergens anders heen te sturen:

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

Het ondertekeningsgeheim is altijd het accountgeheim; alleen de bestemming verandert.

## Gebeurtenissen

| Gebeurtenis                     | Wordt geactiveerd wanneer                                                                     |
| ------------------------------- | --------------------------------------------------------------------------------------------- |
| `project.created`               | Het project bestaat en het transcript is opgeslagen. Dit is je teken om de SRT te downloaden. |
| `project.creation.failed`       | Het project kon niet worden aangemaakt.                                                       |
| `project.translation.completed` | De vertaling is voltooid en de uitvoer is klaar.                                              |
| `project.translation.failed`    | De vertaling is mislukt.                                                                      |
| `project.lipsync.completed`     | Lipsynchronisatie voltooid.                                                                   |
| `project.lipsync.failed`        | Lipsynchronisatie mislukt.                                                                    |

<Note>
  Er is geen afzonderlijke transcriptiegebeurtenis. Transcriptie wordt uitgevoerd tijdens het aanmaken van een project, en een project
  bestaat pas zodra het transcript is opgeslagen — dus `project.created` betekent al dat het transcript
  klaar is om op te halen met [`POST /v1/projects/{projectId}/transcript`](/docs/nl/api-reference/project-transcript).
</Note>

## Payload

Elke aflevering is een `POST` met een JSON-body:

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

| Veld             | Type           | Beschrijving                                                                                                                 |
| ---------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `type`           | string         | Een van de bovenstaande gebeurtenissen.                                                                                      |
| `eventId`        | string         | Uniek per gebeurtenis. Gebruik dit om te ontdubbelen.                                                                        |
| `projectId`      | string         | Het project waar deze gebeurtenis bij hoort.                                                                                 |
| `workflow`       | string         | `transcription` of `translation`.                                                                                            |
| `status`         | string         | `success` of `failed`.                                                                                                       |
| `targetLanguage` | string         | Alleen aanwezig bij taalspecifieke gebeurtenissen.                                                                           |
| `error`          | object \| null | Bij `*.failed`-gebeurtenissen, dezelfde `{ code, message }`-vorm als de REST API. Zie [Foutcodes](/docs/nl/api-reference/errors). |
| `occurredAt`     | string         | ISO 8601 tijdstempel.                                                                                                        |

De payload is bewust beknopt. Roep de REST API aan voor de inhoud zelf.

## De handtekening verifiëren

Elke aflevering bevat deze headers:

| Header                   | Beschrijving                                                                          |
| ------------------------ | ------------------------------------------------------------------------------------- |
| `X-VoiceCheap-Signature` | `sha256=<hex>` — HMAC-SHA256 van de payload, gesleuteld met uw ondertekeningsgeheim.  |
| `X-VoiceCheap-Timestamp` | Unix-seconden, opgenomen in de ondertekende string zodat u herhalingen kunt afwijzen. |
| `X-VoiceCheap-Event-Id`  | Dezelfde waarde als `eventId` in de body, nuttig bij ondersteuningsverzoeken.         |

De handtekening dekt `<timestamp>.<raw body>`, dus deze is bij elke aflevering anders en bewijst zowel
dat het verzoek van VoiceCheap kwam als dat de body tijdens het transport niet is gewijzigd.

```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>
  Verifieer de handtekening voordat u een aflevering vertrouwt. Uw eindpunt is een openbare URL, en de handtekening is
  wat een echte VoiceCheap-gebeurtenis onderscheidt van al het andere dat het bereikt.
</Warning>

## Reageren

Antwoord met een `2xx`-status om te bevestigen. Antwoord binnen **10 seconden** — bevestig eerst en doe
het werk daarna, in plaats van te verwerken voordat u antwoordt.

Een niet-2xx-antwoord of een time-out wordt geregistreerd als een mislukte aflevering en wordt vooralsnog niet opnieuw geprobeerd.

## Het geheim roteren

Selecteer **Roteren** op de API-pagina om een nieuw geheim te genereren. Het vorige stopt onmiddellijk
met werken, dus implementeer het nieuwe geheim op uw server zodra u roteert.

Het verwijderen van de webhook verwijdert zowel het eindpunt als het geheim, en afleveringen stoppen.
