---
title: "Webhook 개요"
description: "ThunderPhone이 실시간 이벤트를 전달하는 방식, 서명을 검증하는 방법, 그리고 레거시 전달 모델과 엔드포인트 기반 전달 모델의 비교를 설명합니다."
---

ThunderPhone은 통화 중 이벤트가 발생할 때 서버로 HTTP `POST` 요청을 전송합니다. 예를 들어 수신 통화가 시작되거나, 통화가 종료되거나, 평가 실행이 완료되거나, 알림이 발생하는 경우입니다. 전달 모델은 **두 가지**입니다.

<CardGroup cols={2}>
  <Card title="Webhook 엔드포인트(권장)" icon="bolt" href="/ko/webhooks/endpoints">
    여러 URL, 엔드포인트별 시크릿, 엔드포인트별 이벤트 필터 및 자동 재시도를 제공합니다.
    `GET/POST/PATCH/DELETE /v1/developer/webhook-endpoints`로 관리합니다.
  </Card>
  <Card title="단일 URL 레거시 webhook" icon="link" href="/api-reference/organizations#legacy-single-url-webhook">
    조직당 URL 하나입니다. **차단형** 구성 교환을 포함한 통화 수명 주기 이벤트를 전달합니다. `GET/PUT /v1/webhook`에서 관리합니다.
  </Card>
</CardGroup>

[이벤트 카탈로그](/ko/webhooks/events)에 나열된 모든 이벤트 유형은 webhook 엔드포인트를 통해 전달됩니다. 6개의 통화 수명 주기 이벤트
(`telephony.incoming`, `telephony.complete`, `telephony.tool`,
`web.incoming`, `web.complete`, `web.tool`)는 레거시 단일 URL webhook에도 **함께** 전송됩니다. 레거시 URL과 일치하는 엔드포인트가 모두 있으면 이벤트를 **두** 경로 모두에서 수신합니다. 차단 동작([`telephony.incoming` / `web.incoming` 구성
교환](/ko/webhooks/call-incoming) 및 webhook 모드
[도구 디스패치](/ko/tools/overview))은 레거시 경로에서만 제공됩니다. 모든 엔드포인트 전달은 응답을 기다리지 않고 전송하는 알림입니다.

## 페이로드 형식

엔드포인트 전달은 `data`, `event_id`, `type`를 포함하는 JSON 객체입니다.

```json
{
  "data": {
    "call_id": 987654321,
    "from_number": "+14155550199",
    "to_number": "+15551234567"
  },
  "event_id": "3f6b2ad0-1c9e-4a57-9f2b-8f6f0f9d2f11",
  "type": "telephony.incoming"
}
```

`event_id`는 발생한 이벤트마다 고유합니다. 재시도 간에도, 이벤트를 수신하는 모든 엔드포인트 간에도 **동일하므로**, 이를 기준으로 중복을 제거합니다.

레거시 단일 URL webhook은 동일한 `type` 및 `data`를 전송하지만 `event_id`는 **포함하지 않습니다**.

```json
{
  "type": "telephony.incoming",
  "data": { "call_id": 987654321, "from_number": "+14155550199", "to_number": "+15551234567" }
}
```

전송 시 모든 본문은 키를 알파벳순으로 정렬하고 공백 없이 UTF-8로 표준 직렬화됩니다. 이 문서의 보기 좋게 정렬된 예시는 가독성을 위한 것입니다.

전체 이벤트 유형 및 페이로드 필드 목록은 [이벤트 카탈로그](/ko/webhooks/events)를 참조하십시오.

## 서명 검증

모든 요청에는 두 개의 서명이 포함되며, 둘 다 엔드포인트의 `secret`(레거시 전송의 경우 조직 수준 웹훅 `secret`)을 키로 사용하는 HMAC-SHA256입니다.

- `X-ThunderPhone-Signature-V2: t=<unix seconds>,v1=<hex>`는 타임스탬프, 메서드, 경로 및 쿼리, 원시 본문에 서명합니다. 이 서명을 검증하고, 캡처된 요청이 나중에 재전송되지 않도록 `t`가 현재 시간보다 **5분** 이상 차이 나는 요청은 거부합니다. 재시도 요청은 전송될 때 다시 서명됩니다.
- `X-ThunderPhone-Signature`는 원시 본문에만 서명합니다. 기존 검증기가 계속 작동하도록 변경 없이 계속 전송됩니다.

### 단계

1. 파싱하기 **전에** 헤더, 메서드, 원시 경로 및 쿼리, 원시 본문을 읽습니다.
2. 시크릿을 사용하여
   `<t>\n<METHOD>\n<path and query>\n<body>`의 16진수 HMAC-SHA256을 계산합니다.
3. `t`가 300초를 초과하여 차이 나거나, 상수 시간 비교에서 결과가 `v1`과 일치하지 않으면 요청을 거부합니다.

본문은 정규 JSON 직렬화(키 정렬, 압축 구분자)이며, 전송하는 바이트를 정확히 서명합니다.
[검증 가이드](/ko/guides/verify-webhook-signatures)에는 정확한 서명 문자열, 예제, SDK 없이 작성하는 코드 및 레거시 헤더가 포함되어 있습니다.

<CodeGroup>
```python Python (Flask)
from flask import Flask, abort, request
from thunderphone import verify_signature

app = Flask(__name__)

@app.post("/thunderphone-webhook")
def handle():
    query = request.query_string.decode()
    if not verify_signature(
        request.headers.get("X-ThunderPhone-Signature-V2"),
        secret=WEBHOOK_SECRET,
        method=request.method,
        path=request.path + (f"?{query}" if query else ""),
        body=request.get_data(),  # raw bytes
    ):
        abort(401)
    event = request.get_json()
    # dispatch on event["type"] …
    return "", 204
```

```javascript Node.js (Express)
import express from "express";
import { verifySignature } from "@thunderphone/sdk";

const app = express();
app.post(
  "/thunderphone-webhook",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const ok = verifySignature({
      header: req.header("X-ThunderPhone-Signature-V2"),
      secret: process.env.WEBHOOK_SECRET,
      method: req.method,
      path: req.originalUrl,
      body: req.body,
    });
    if (!ok) return res.sendStatus(401);
    const event = JSON.parse(req.body.toString("utf8"));
    // dispatch on event.type …
    res.sendStatus(204);
  },
);
```
</CodeGroup>

## 전달 의미론

이러한 의미론은 **엔드포인트** 전송에 적용됩니다. 레거시 단일 URL
웹훅은 재시도 없이 한 번 동기적으로 시도합니다. 단, 전화 통화
[`telephony.incoming`](/ko/webhooks/call-incoming)
구성 교환은 한 번 재시도합니다.

<AccordionGroup>
  <Accordion title="재시도">
    각 이벤트는 즉시 한 번 시도됩니다. 모든 `2xx` 응답은
    전송을 확인합니다. 그 외 결과(2xx가 아님,
    연결 오류, 시간 초과)가 발생하면 **첫 번째 시도 후 1분, 5분, 30분, 2시간, 6시간,
    12시간 및 24시간**에 재시도합니다. 즉, 24시간에 걸쳐 총 8회 시도합니다.
    모든 시도가 실패하면 전송이 중지되고 엔드포인트는
    [웹훅 엔드포인트](/ko/webhooks/endpoints)에서 `status="failing"`으로
    표시됩니다. 조직의 소유자와 관리자에게 이메일을 보내고
    대시보드에도 표시합니다. 페이로드가 안정적으로 수락되는 즉시 `2xx`를 반환하고
    비동기적으로 처리합니다.
    엔드포인트 테스트 API의 테스트 전송은 첫 번째 시도 후 약 1분 뒤에 한 번 재시도되며
    엔드포인트 상태는 변경하지 않습니다.
  </Accordion>

  <Accordion title="순서">
    전달 순서는 최선 노력 방식입니다. 실제로는 이벤트가 발생한
    순서대로 전달하지만, 실패 시 재시도로 인해 순서가 바뀔 수 있습니다.
    항상 `call_id` / 객체 ID를 기준으로 중복을 제거하고 조정하십시오.
  </Accordion>

  <Accordion title="중복">
    전달은 **최소 한 번** 방식입니다. 수신하지 못한 응답 이후의 재시도로 인해
    이벤트가 중복될 수 있습니다. 모든 재시도에는 동일한
    `event_id`가 포함되므로 처리한 ID를 저장하고 반복 항목을 건너뛰십시오. `event_id`는
    엔드포인트 간에도 공유됩니다. 동일한 이벤트를 구독하는 두 엔드포인트는
    같은 `event_id`를 수신합니다.
  </Accordion>

  <Accordion title="시간 초과">
    엔드포인트 전송은 시도당 **30초**의 시간 초과가 적용됩니다. 레거시
    경로에서 실시간 통화 동작을 제어하는 차단 요청, 즉
    [`telephony.incoming` / `web.incoming`](/ko/webhooks/call-incoming)
    구성 교환은 전화 통화의 첫 번째 시도에 **10초**로 제한되며,
    한 번의 재시도를 포함한 전체 교환에는 **13초**로 제한됩니다.
    위젯 세션은 **10초**로 제한됩니다. 응답이 느리면 통화 수신이 지연되므로
    몇 초 내에 응답하는 것을 목표로 하십시오. 웹훅 모드의 [도구 디스패치](/ko/tools/overview)는
    기본적으로 20초를 허용하며, 도구 선언에서 최상위 수준의 `timeout`을 설정할 수 있습니다.
  </Accordion>

  <Accordion title="소스 IP">
    아웃바운드 웹훅은 ThunderPhone 클라우드 IP 범위에서 발신됩니다.
    방화벽에서 허용 목록이 필요하면 지원팀에 문의하십시오. 현재 범위를
    공유해 드립니다.
  </Accordion>
</AccordionGroup>

## 레거시 웹훅과 엔드포인트 기반 웹훅 선택

| 기능 | 레거시 (`/v1/webhook`) | 엔드포인트 (`/v1/developer/webhook-endpoints`) |
|---------|------------------------|----------------------------------------------|
| URL 수 | 조직당 1개 | 조직당 여러 개 |
| 이벤트 범위 | `telephony.*` / `web.*`만 | 10가지 이벤트 유형 모두 |
| 이벤트 필터 | — | 엔드포인트별 |
| 재시도 | 전화 통화 구성 교환에 대한 1회를 제외하면 없음 | 24시간 동안 8회 시도 |
| 봉투 | `type` + `data` | `type` + `data` + `event_id` |
| 시크릿 교체 | 단일 시크릿 교체 | 엔드포인트별 시크릿 |
| 삭제 없이 비활성화 | `PUT /v1/webhook`와 `{"url": ""}` 사용 | `status=disabled` |
| 상태 표시 | — | `active` / `disabled` / `failing` |
| 차단형 구성 교환 | 예 ([`telephony.incoming` / `web.incoming`](/ko/webhooks/call-incoming)) | 없음 — 알림 전용 |
| 적합한 용도 | 동적 통화 구성 | 프로덕션 환경의 이벤트 수신 |

새 통합에서는 엔드포인트 기반 웹훅을 통해 이벤트를 소비해야 합니다.
통화 연결 시점에 통화를 동적으로 구성하거나 웹훅 모드 도구 디스패치를
사용하는 경우에만 레거시 URL을 유지하거나 추가하십시오. 이러한
요청/응답 교환은 레거시 경로에서만 실행됩니다.

---

## 관련 항목

<CardGroup cols={2}>
  <Card title="이벤트 카탈로그" icon="list" href="/ko/webhooks/events">
    모든 이벤트 유형과 해당 페이로드입니다.
  </Card>
  <Card title="웹훅 엔드포인트" icon="bolt" href="/ko/webhooks/endpoints">
    여러 엔드포인트, 이벤트 필터 및 시크릿을 관리합니다.
  </Card>
  <Card title="telephony.incoming / web.incoming" icon="phone" href="/ko/webhooks/call-incoming">
    통화를 구성하기 위해 서버에서 반드시 응답해야 하는 차단 요청입니다.
  </Card>
  <Card title="telephony.complete / web.complete" icon="phone" href="/ko/webhooks/call-complete">
    트랜스크립트, 녹음 및 측정항목이 포함된 통화 후 페이로드입니다.
  </Card>
</CardGroup>
