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

> Reciba una devolución de llamada HTTP en el momento en que un proyecto alcance un hito, en lugar de realizar sondeos

# Webhooks

Los webhooks permiten que VoiceCheap notifique a su servidor tan pronto como ocurra algo, para que pueda dejar de realizar sondeos
`GET /v1/translate/{projectId}/status`.

<Note>
  Las entregas se intentan **una vez**. Aún no hay reintentos, así que mantenga el sondeo como una red de seguridad para
  cualquier cosa que no pueda permitirse perder. Se planean reintentos con retroceso exponencial.
</Note>

## Configuración

1. Abra la [página de API](https://voicecheap.ai/page-api) en su cuenta de VoiceCheap.
2. En **Webhooks**, seleccione **Generar secreto de firma**. El secreto comienza con `whsec_` y se muestra
   **una vez**: cópielo y guárdelo en su servidor.
3. Ingrese su **URL de punto final** y guárdela. Debe usar `https`.

Eso es todo. Cada proyecto que inicie a partir de entonces enviará eventos a ese punto final.

### Anulación del punto final por solicitud

`POST /v1/translate` y `POST /v1/projects` aceptan un campo opcional `webhookUrl` que anula el
punto final de la cuenta solo para ese proyecto. Es útil para enviar tráfico de prueba a otro lugar:

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

El secreto de firma es siempre el secreto de la cuenta; solo cambia el destino.

## Eventos

| Evento                          | Se dispara cuando                                                                              |
| ------------------------------- | ---------------------------------------------------------------------------------------------- |
| `project.created`               | El proyecto existe y su transcripción está almacenada. Esta es su señal para descargar el SRT. |
| `project.creation.failed`       | El proyecto no pudo ser creado.                                                                |
| `project.translation.completed` | La traducción finalizó y los resultados están listos.                                          |
| `project.translation.failed`    | La traducción falló.                                                                           |
| `project.lipsync.completed`     | Sincronización labial finalizada.                                                              |
| `project.lipsync.failed`        | La sincronización labial falló.                                                                |

<Note>
  No existe un evento de transcripción independiente. La transcripción se ejecuta durante la creación del proyecto, y un proyecto
  solo existe una vez que su transcripción se almacena; por lo tanto, `project.created` ya significa que la transcripción está
  lista para obtenerse con [`POST /v1/projects/{projectId}/transcript`](/docs/es/api-reference/project-transcript).
</Note>

## Carga útil

Cada entrega es un `POST` con un cuerpo 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"
}
```

| Campo            | Tipo           | Descripción                                                                                                                       |
| ---------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `type`           | string         | Uno de los eventos anteriores.                                                                                                    |
| `eventId`        | string         | Único por evento. Úselo para eliminar duplicados.                                                                                 |
| `projectId`      | string         | El proyecto al que pertenece este evento.                                                                                         |
| `workflow`       | string         | `transcription` o `translation`.                                                                                                  |
| `status`         | string         | `success` o `failed`.                                                                                                             |
| `targetLanguage` | string         | Presente solo en eventos específicos del idioma.                                                                                  |
| `error`          | object \| null | En eventos `*.failed`, la misma forma `{ code, message }` que la API REST. Consulte [Códigos de error](/docs/es/api-reference/errors). |
| `occurredAt`     | string         | Marca de tiempo ISO 8601.                                                                                                         |

La carga útil es intencionalmente ligera. Llame a la API REST para obtener el contenido en sí.

## Verificación de la firma

Cada entrega incluye estos encabezados:

| Encabezado               | Descripción                                                                         |
| ------------------------ | ----------------------------------------------------------------------------------- |
| `X-VoiceCheap-Signature` | `sha256=<hex>` — HMAC-SHA256 de la carga útil, codificado con su secreto de firma.  |
| `X-VoiceCheap-Timestamp` | Segundos Unix, incluidos en la cadena firmada para que pueda rechazar repeticiones. |
| `X-VoiceCheap-Event-Id`  | El mismo valor que `eventId` en el cuerpo, útil en solicitudes de soporte.          |

La firma cubre `<timestamp>.<raw body>`, por lo que es diferente en cada entrega y demuestra tanto
que la solicitud provino de VoiceCheap como que el cuerpo no fue modificado durante el tránsito.

```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>
  Verifique la firma antes de confiar en una entrega. Su punto de conexión es una URL pública, y la firma es
  lo que distingue un evento real de VoiceCheap de cualquier otra cosa que llegue a él.
</Warning>

## Respuesta

Responda con cualquier estado `2xx` para confirmar la recepción. Responda en un plazo de **10 segundos**: confirme primero y realice
el trabajo después, en lugar de procesar antes de responder.

Una respuesta que no sea 2xx o un tiempo de espera se registra como una entrega fallida y, por ahora, no se vuelve a intentar.

## Rotación del secreto

Seleccione **Rotar** en la página de la API para generar un nuevo secreto. El anterior deja de funcionar
inmediatamente, así que implemente el nuevo secreto en su servidor tan pronto como lo rote.

Eliminar el webhook elimina tanto el punto de conexión como el secreto, y las entregas se detienen.
