telephony.complete / web.complete
통화가 종료될 때 전송되는 비차단 웹훅으로, 트랜스크립트, 녹음 URL 및 지표를 포함합니다.
모든 통화가 종료된 후 완료 이벤트가 발생합니다. 인바운드 전화 통화, 아웃바운드 전화 통화, 웹 통화 또는 테스트 통화(빌더 마이크 세션)가 포함됩니다. 이는 비차단 방식입니다. 2xx로 응답하면 됩니다.
이 이벤트는 두 경로 모두로 전송됩니다.
- **웹훅 엔드포인트**는
telephony.complete(전화 통화) 또는web.complete(웹 통화 및 빌더 마이크 테스트 통화)를 수신합니다. 아래에 문서화된 안정적인 페이로드, 전송별event_id, 30초 타임아웃 및 최대 24시간 동안의 재시도가 포함됩니다. - **레거시 단일 URL 웹훅**은 약간 다른 페이로드로 동기식 시도를 한 번 수신합니다(10초 타임아웃, 재시도 없음). 자세한 내용은 레거시 페이로드 차이점을 참조하십시오.
요청 페이로드(엔드포인트 전송)
{
"data": {
"billable_minutes": 1.25,
"billing_total_cents": 8,
"call_id": 987654321,
"direction": "inbound",
"duration_seconds": 54,
"end_reason": "user_hangup",
"end_time": "2026-04-20T18:25:04.822Z",
"from_number": "+14155550199",
"product": "spark",
"recording_url": "https://storage.example.com/…",
"start_time": "2026-04-20T18:24:10.113Z",
"status": "completed",
"to_number": "+15551234567",
"transcripts": [ /* see Transcript format */ ],
"transfer_number": null,
"voice": "john"
},
"event_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
"type": "telephony.complete"
}| 필드 | 유형 | 설명 |
|---|---|---|
call_id | integer | 이 통화의 모든 이벤트에서 동일하게 유지됩니다 |
direction | string | inbound, outbound, web, test. 과거 페이로드에는 레거시 mic 또는 widget 값이 포함될 수 있습니다 |
from_number, to_number | string | E.164. 웹 통화 및 테스트 통화에서 from_number는 리터럴 "web"입니다 |
origin_domain | string | 웹/테스트 전용 — 위젯을 호스팅한 페이지 오리진(마이크 세션에서는 비어 있음) |
start_time, end_time | timestamp | ISO 8601 UTC |
duration_seconds | integer | null | 시작/종료 시간에서 파생됩니다 |
status | string | completed 또는 failed |
end_reason | string | 아래 표를 참조하십시오 |
product, voice | string | 통화 시점에 적용된 에이전트 구성 |
transfer_number | string | null | 통화가 전환된 경우 설정됩니다 |
recording_url | string | null | 만료되는 서명 URL입니다. 즉시 다운로드하십시오. 녹음 아티팩트를 사용할 수 없는 경우 null입니다 |
billable_minutes | number | 청구된 분 단위로, 가장 가까운 0.25분으로 반올림됩니다(15초 단위, 최소 0.25). 바로 음성사서함으로 연결된 통화도 실제 계량된 분이 여기에 보고되지만, 요금은 플랜 요율 기준으로 최대 1분으로 제한됩니다. |
billing_total_cents | integer | 미국 달러 센트 |
transcripts | array | 턴별 트랜스크립트 항목입니다. 트랜스크립트를 사용할 수 없는 경우 비어 있을 수 있습니다 |
종료 사유
| 값 | 의미 |
|---|---|
user_hangup | 상대방이 먼저 전화를 끊었습니다 |
ai_hangup | AI가 의도적으로 통화를 종료했습니다 |
ai_transfer | AI가 통화를 전환했습니다. transfer_number가 설정됩니다 |
ai_warm_transfer | AI가 웜 전환(상담원 연결 후 전환)을 완료했습니다 |
voicemail_hangup | 음성사서함이 감지되었으며 voicemail_action에 따라 통화가 종료되었습니다 |
max_duration | 통화가 최대 통화 시간 제한에 도달했습니다 |
superseded | 세션이 더 최신 세션으로 대체되었습니다 |
unknown | 종료 사유를 확인할 수 없습니다 |
트랜스크립트 형식
transcripts의 각 항목은 하나의 대화 차례입니다. 역할은
user(발신자 음성), model(에이전트 음성 및 도구 호출),
tool(도구 결과), system(언어 전환 등의 통화 이벤트)입니다.
[
{
"role": "user",
"content_type": "text/plain",
"content": "Hi, I'm calling about my appointment.",
"start_ms": 1200,
"end_ms": 4100,
"audio_url": "https://storage.example.com/…"
},
{
"role": "model",
"content_type": "text/plain",
"content": "Sure, what date works best?",
"start_ms": 4200,
"end_ms": 6100
},
{
"role": "model",
"content_type": "application/json",
"content": {
"tool_call": "search_appointments",
"arguments": { "date": "2026-04-21" }
}
},
{
"role": "tool",
"content_type": "application/json",
"content": {
"tool_name": "search_appointments",
"response": { "available_slots": ["9:00 AM", "2:00 PM"] }
}
}
]| 필드 | 유형 | 설명 |
|---|---|---|
role | 문자열 | user, model, tool 또는 system |
content_type | 문자열 | 음성에는 text/plain, 도구 호출, 도구 결과 및 시스템 이벤트에는 application/json |
content | 문자열 | 객체 | 음성 텍스트 또는 위에 표시된 구조화된 객체입니다. 도구 호출: {"tool_call": name, "arguments": {…}}. 도구 결과: {"tool_name": name, "response": {…}} |
start_ms, end_ms | 정수 | 통화 시작 시점부터의 오프셋(밀리초)입니다. 오디오 타이밍을 알 수 있는 경우 제공됩니다. |
ttfa_ms | 정수 | 측정된 경우 model 차례의 첫 오디오까지 걸린 시간입니다. |
audio_url, audio_urls | 문자열 / 배열 | 차례별로 녹음된 경우 해당 차례 오디오의 만료되는 서명 URL입니다. |
중단 마커, 맞장구 프롬프트 및 원시 위치를 포함한 완전히 구조화된 차례 기록은
GET /v1/calls/{call_id}/history를 사용합니다.
레거시 페이로드 차이점
레거시 단일 URL 웹훅 엔벌로프는
{"type": "telephony.complete" | "web.complete", "data": {…}}이며,
event_id가 없고 data가 엔드포인트 페이로드와 다릅니다.
- 차례 배열은
transcripts가 아닌history아래에 있습니다(위와 동일한 차례 스키마). - 필드 집합은 원시 통화 종료 보고서이며 위 표에 없는 추가 내부 필드를 포함할 수 있습니다. 알 수 없는 필드는 정보용으로 처리합니다.
- 웹 통화(
direction: "web")는from_number/to_number를 생략하고origin_domain을 추가합니다. - Builder 마이크 테스트 통화는 레거시 경로에서
telephony.complete로 보고됩니다(엔드포인트 시스템은 이를web.complete로 매핑합니다). - 전환 조정: 통화가 전환으로 종료되면 레거시 웹훅이 동기적으로 호출되며, 인계 대상이 준비되지 않았음을 알리기 위해
{"transfer_ready": false}를 응답할 수 있습니다. 그 외 모든 응답(또는 레거시 웹훅 없음)은 전환을 진행합니다. 엔드포인트 전송은 이를 위해 참조되지 않습니다.
핸들러 예시
import hashlib
import hmac
import json
import os
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
SECRET = os.environ["THUNDERPHONE_WEBHOOK_SECRET"]
def verify(body: bytes, signature: str) -> bool:
expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")
@app.post("/thunderphone-webhook")
async def webhook(request: Request):
body = await request.body()
if not verify(body, request.headers.get("X-ThunderPhone-Signature", "")):
raise HTTPException(status_code=401)
event = json.loads(body)
if event["type"] in ("telephony.complete", "web.complete"):
data = event["data"]
# Endpoint deliveries use "transcripts"; the legacy webhook uses "history".
turns = data.get("transcripts") or data.get("history") or []
await persist_call_record(
call_id=data["call_id"],
turns=turns,
recording_url=data.get("recording_url"),
)
if data["end_reason"] in ("ai_transfer", "ai_warm_transfer"):
await notify_team(data.get("transfer_number"), data["call_id"])
return {"ok": True}import crypto from "node:crypto";
import express from "express";
const app = express();
const SECRET = process.env.THUNDERPHONE_WEBHOOK_SECRET;
function verify(body, signature) {
const expected = crypto.createHmac("sha256", SECRET).update(body).digest("hex");
return signature &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
app.post(
"/thunderphone-webhook",
express.raw({ type: "application/json" }),
async (req, res) => {
if (!verify(req.body, req.header("X-ThunderPhone-Signature"))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
if (["telephony.complete", "web.complete"].includes(event.type)) {
const data = event.data;
// Endpoint deliveries use "transcripts"; the legacy webhook uses "history".
const turns = data.transcripts ?? data.history ?? [];
await persistCallRecord({ ...data, turns });
if (["ai_transfer", "ai_warm_transfer"].includes(data.end_reason)) {
await notifyTeam(data.transfer_number, data.call_id);
}
}
res.json({ ok: true });
},
);일반적인 사용 사례
CRM 통합
각 통화의 트랜스크립트와 녹음 URL을 고객 기록과 함께 저장합니다.
분석
주제 모델링, CSAT 신호 추출 또는 전환율 모니터링을 위해 트랜스크립트를 파이프라인으로 스트리밍합니다.
품질 검토
사람이 검토할 수 있도록 QA 도구에서 통화를 열거나, 자체 평가 모델로 처리합니다.
알림
전환 또는 실패 시 담당 팀원에게 알림을 보냅니다.