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

> Erhalten Sie einen HTTP-Callback, sobald ein Projekt einen Meilenstein erreicht, anstatt Polling zu verwenden

# Webhooks

Webhooks ermöglichen es VoiceCheap, Ihren Server zu benachrichtigen, sobald etwas passiert, sodass Sie das Polling beenden können
`GET /v1/translate/{projectId}/status`.

<Note>
  Zustellungen werden **einmal** versucht. Es gibt noch keine Wiederholungsversuche, also behalten Sie das Polling als Sicherheitsnetz bei für
  alles, was Sie nicht verpassen dürfen. Wiederholungsversuche mit exponentiellem Backoff sind geplant.
</Note>

## Einrichtung

1. Öffnen Sie die [API-Seite](https://voicecheap.ai/page-api) in Ihrem VoiceCheap-Konto.
2. Wählen Sie unter **Webhooks** die Option **Signatur-Geheimnis generieren**. Das Geheimnis beginnt mit `whsec_` und wird
   **einmal** angezeigt — kopieren Sie es und speichern Sie es auf Ihrem Server.
3. Geben Sie Ihre **Endpunkt-URL** ein und speichern Sie diese. Sie muss `https` verwenden.

Das ist alles. Jedes Projekt, das Sie von da an starten, liefert Ereignisse an diesen Endpunkt.

### Überschreiben des Endpunkts pro Anfrage

`POST /v1/translate` und `POST /v1/projects` akzeptieren ein optionales `webhookUrl`-Feld, das den
Konto-Endpunkt nur für dieses Projekt überschreibt. Dies ist praktisch, um Staging-Datenverkehr woanders hinzuleiten:

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

Das Signatur-Geheimnis ist immer das Konto-Geheimnis; nur das Ziel ändert sich.

## Ereignisse

| Ereignis                        | Löst aus, wenn                                                                                           |
| ------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `project.created`               | Das Projekt existiert und sein Transkript ist gespeichert. Dies ist Ihr Signal, die SRT herunterzuladen. |
| `project.creation.failed`       | Das Projekt konnte nicht erstellt werden.                                                                |
| `project.translation.completed` | Die Übersetzung ist abgeschlossen und die Ausgaben sind bereit.                                          |
| `project.translation.failed`    | Die Übersetzung ist fehlgeschlagen.                                                                      |
| `project.lipsync.completed`     | Lippensynchronisation abgeschlossen.                                                                     |
| `project.lipsync.failed`        | Lippensynchronisation fehlgeschlagen.                                                                    |

<Note>
  Es gibt kein separates Transkriptionsereignis. Die Transkription läuft innerhalb der Projekterstellung ab, und ein Projekt
  existiert erst, sobald das Transkript gespeichert ist — daher bedeutet `project.created` bereits, dass das Transkript
  bereit zum Abrufen mit [`POST /v1/projects/{projectId}/transcript`](/docs/de/api-reference/project-transcript) ist.
</Note>

## Nutzlast

Jede Zustellung ist ein `POST` mit einem 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"
}
```

| Feld             | Typ            | Beschreibung                                                                                                                         |
| ---------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `type`           | string         | Eines der oben genannten Ereignisse.                                                                                                 |
| `eventId`        | string         | Eindeutig pro Ereignis. Verwenden Sie dies zur Deduplizierung.                                                                       |
| `projectId`      | string         | Das Projekt, zu dem dieses Ereignis gehört.                                                                                          |
| `workflow`       | string         | `transcription` oder `translation`.                                                                                                  |
| `status`         | string         | `success` oder `failed`.                                                                                                             |
| `targetLanguage` | string         | Nur bei sprachspezifischen Ereignissen vorhanden.                                                                                    |
| `error`          | object \| null | Bei `*.failed`-Ereignissen die gleiche `{ code, message }`-Form wie bei der REST-API. Siehe [Fehlercodes](/docs/de/api-reference/errors). |
| `occurredAt`     | string         | ISO 8601 Zeitstempel.                                                                                                                |

Die Nutzlast ist absichtlich schlank gehalten. Rufen Sie die REST-API für den Inhalt selbst auf.

## Überprüfung der Signatur

Jede Zustellung enthält diese Header:

| Header                   | Beschreibung                                                                                |
| ------------------------ | ------------------------------------------------------------------------------------------- |
| `X-VoiceCheap-Signature` | `sha256=<hex>` — HMAC-SHA256 der Nutzlast, verschlüsselt mit Ihrem Signatur-Secret.         |
| `X-VoiceCheap-Timestamp` | Unix-Sekunden, enthalten in der signierten Zeichenfolge, damit Sie Replays ablehnen können. |
| `X-VoiceCheap-Event-Id`  | Gleicher Wert wie `eventId` im Body, nützlich bei Support-Anfragen.                         |

Die Signatur deckt `<timestamp>.<raw body>` ab, daher ist sie bei jeder Zustellung anders und beweist sowohl,
dass die Anfrage von VoiceCheap kam, als auch, dass der Body während der Übertragung nicht verändert wurde.

```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>
  Überprüfen Sie die Signatur, bevor Sie einer Zustellung vertrauen. Ihr Endpunkt ist eine öffentliche URL, und die Signatur ist
  das, was ein echtes VoiceCheap-Ereignis von allem anderen unterscheidet, das ihn erreicht.
</Warning>

## Antworten

Antworten Sie mit einem beliebigen `2xx`-Status zur Bestätigung. Antworten Sie innerhalb von **10 Sekunden** — bestätigen Sie zuerst und erledigen
Sie die Arbeit danach, anstatt sie vor der Antwort zu verarbeiten.

Eine Nicht-2xx-Antwort oder ein Timeout wird als fehlgeschlagene Zustellung aufgezeichnet und wird derzeit nicht erneut versucht.

## Rotation des Secrets

Wählen Sie **Rotieren** auf der API-Seite, um ein neues Secret zu generieren. Das vorherige funktioniert sofort nicht mehr,
also stellen Sie das neue Secret auf Ihrem Server bereit, sobald Sie rotieren.

Das Löschen des Webhooks entfernt sowohl den Endpunkt als auch das Secret, und die Zustellungen stoppen.
