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

# Webhook

> ポーリングを行う代わりに、プロジェクトがマイルストーンに達した瞬間にHTTPコールバックを受け取ります

# Webhook

Webhookを使用すると、何かが発生した瞬間にVoiceCheapがサーバーに通知できるため、ポーリングを停止できます
`GET /v1/translate/{projectId}/status`.

<Note>
  配信は**1回**試行されます。現時点では再試行機能がないため、安全策としてポーリングを継続してください。
  見逃せない情報については特に注意が必要です。指数バックオフを用いた再試行機能は今後実装予定です。
</Note>

## セットアップ

1. VoiceCheapアカウントで[APIページ](https://voicecheap.ai/page-api)を開きます。
2. **Webhooks**で、**Generate signing secret**を選択します。シークレットは`whsec_`で始まり、表示されるのは
   **1回のみ**です。コピーしてサーバーに保存してください。
3. **endpoint 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/ja/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`       | 文字列            | `transcription` または `translation`。                                                                       |
| `status`         | 文字列            | `success` または `failed`。                                                                                  |
| `targetLanguage` | string         | 言語固有のイベントでのみ存在します。                                                                                       |
| `error`          | object \| null | `*.failed` イベントでは、REST API と同じ `{ code, message }` 形状になります。[エラーコード](/docs/ja/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を削除すると、エンドポイントとシークレットの両方が削除され、配信が停止します。
