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

# 웹훅

> 폴링 대신 프로젝트가 마일스톤에 도달하는 즉시 HTTP 콜백을 수신합니다

# 웹훅

웹훅을 사용하면 폴링을 중단하고 이벤트가 발생하는 즉시 VoiceCheap가 귀하의 서버에 알림을 보내도록 할 수 있습니다
`GET /v1/translate/{projectId}/status`.

<Note>
  전송은 **한 번** 시도됩니다. 아직 재시도 기능이 없으므로 안전망으로 폴링을 유지하세요.
  놓쳐서는 안 되는 모든 항목에 대해 그렇습니다. 지수 백오프를 사용한 재시도가 계획되어 있습니다.
</Note>

## 설정

1. VoiceCheap 계정에서 [API 페이지](https://voicecheap.ai/page-api)을(를) 엽니다.
2. **Webhooks**에서 **Generate signing secret**을 선택합니다. 시크릿은 `whsec_`(으)로 시작하며
   **한 번만** 표시되므로 복사하여 서버에 저장하세요.
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/ko/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` | 문자열        | 언어별 이벤트에서만 제공됩니다.                                                                                    |
| `error`          | 객체 \| null | `*.failed` 이벤트 시, REST API와 동일한 `{ code, message }` 형태입니다. [오류 코드](/docs/ko/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)\*\*를 선택하여 새 시크릿을 생성하세요. 이전 시크릿은
즉시 작동이 중단되므로, 교체 즉시 새 시크릿을 서버에 배포하세요.

웹훅을 삭제하면 엔드포인트와 시크릿이 모두 제거되며 전송이 중단됩니다.
