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

> 在项目达到里程碑时立即接收 HTTP 回调，无需轮询

# Webhooks

Webhooks 让 VoiceCheap 能够在发生情况时立即通知您的服务器，因此您可以停止轮询
`GET /v1/translate/{projectId}/status`.

<Note>
  交付尝试 **一次**。目前还没有重试机制，因此请保留轮询作为安全网，以应对
  任何您不能错过的内容。计划支持指数退避重试。
</Note>

## 设置

1. 在您的 VoiceCheap 账户中打开 [API 页面](https://voicecheap.ai/page-api)。
2. 在 **Webhooks** 中，选择 **生成签名密钥**。该密钥以 `whsec_` 开头，仅显示
   **一次** — 请复制并将其存储在您的服务器上。
3. 输入您的 **端点 URL** 并保存。它必须使用 `https`。

就是这样。从那时起，您启动的每个项目都会向该端点发送事件。

### 按请求覆盖端点

`POST /v1/translate` 和 `POST /v1/projects` 接受一个可选的 `webhookUrl` 字段，该字段仅覆盖
该项目的账户端点。这对于将暂存流量发送到其他地方非常方便：

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

签名密钥始终是账户密钥；只有目标会发生变化。

## 事件

| 事件                              | 触发条件                          |
| ------------------------------- | ----------------------------- |
| `project.created`               | 项目已存在且其转录内容已存储。这是您下载 SRT 的提示。 |
| `project.creation.failed`       | 无法创建项目。                       |
| `project.translation.completed` | 翻译完成且输出已就绪。                   |
| `project.translation.failed`    | 翻译失败。                         |
| `project.lipsync.completed`     | 口型同步已完成。                      |
| `project.lipsync.failed`        | 口型同步失败。                       |

<Note>
  没有单独的转录事件。转录在项目创建过程中运行，而项目
  仅在转录内容存储后才存在——因此 `project.created` 已经意味着转录内容
  已准备好通过 [`POST /v1/projects/{projectId}/transcript`](/docs/zh/api-reference/project-transcript) 获取。
</Note>

## 有效载荷

每次交付都是一个带有 JSON 主体的 `POST`：

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

| 字段               | 类型             | 描述                                                                                             |
| ---------------- | -------------- | ---------------------------------------------------------------------------------------------- |
| `type`           | string         | 上述事件之一。                                                                                        |
| `eventId`        | string         | 每个事件唯一。用于去重。                                                                                   |
| `projectId`      | string         | 此事件所属的项目。                                                                                      |
| `workflow`       | string         | `transcription` 或 `translation`。                                                               |
| `status`         | string         | `success` 或 `failed`。                                                                          |
| `targetLanguage` | string         | 仅在特定语言事件中出现。                                                                                   |
| `error`          | object \| null | 在 `*.failed` 事件中，与 REST API 具有相同的 `{ code, message }` 形状。请参阅 [错误代码](/docs/zh/api-reference/errors)。 |
| `occurredAt`     | string         | ISO 8601 时间戳。                                                                                  |

有效载荷特意保持精简。请调用 REST API 获取内容本身。

## 验证签名

每次交付都带有以下标头：

| 标头                       | 描述                                            |
| ------------------------ | --------------------------------------------- |
| `X-VoiceCheap-Signature` | `sha256=<hex>` — 有效载荷的 HMAC-SHA256，由您的签名密钥加密。 |
| `X-VoiceCheap-Timestamp` | Unix 秒数，包含在签名字符串中，以便您可以拒绝重放。                  |
| `X-VoiceCheap-Event-Id`  | 与主体中的 `eventId` 值相同，在支持请求中很有用。                |

签名涵盖了 `<timestamp>.<raw body>`，因此在每次交付时都不同，并证明了
请求来自 VoiceCheap 且主体在传输过程中未被修改。

```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>
  在信任交付之前验证签名。您的端点是一个公共 URL，而签名是
  区分真实的 VoiceCheap 事件与到达该端点的任何其他内容的依据。
</Warning>

## 响应

回复任何 `2xx` 状态以进行确认。请在 **10 秒内** 回复 — 先确认并
在之后执行工作，而不是在回复之前进行处理。

非 2xx 响应或超时将被记录为交付失败，目前不会重试。

## 轮换密钥

在 API 页面上选择 **轮换 (Rotate)** 以生成新密钥。之前的密钥会立即
停止工作，因此请在轮换后尽快将新密钥部署到您的服务器。

删除 webhook 会同时移除端点和密钥，交付将停止。
