telephony.incoming / web.incoming
수신 통화의 구성을 실시간으로 설정하는 차단 웹훅입니다.
수신 전화가 할당된 에이전트가 없는 번호에 도달하거나 웹 위젯 세션이
mode="webhook"의 공개 키에서 시작되면, ThunderPhone은
레거시 웹훅 URL로 차단형
telephony.incoming / web.incoming 요청을 전송하고 구성 응답을 최대 10초 동안 기다립니다. 이
교환을 사용하여 통화별로 프롬프트, 음성, 도구를 동적으로 선택합니다. 전체 패턴은
동적 통화 구성 가이드를 참조하세요.
차단형 교환에는 대체 경로가 없습니다. 핸들러가 2xx가 아닌 상태를 반환하거나,
시간이 초과되거나, 검증에 실패한 구성을 반환하면 통화가 거부됩니다(전화 통화는 연결되지
않으며 위젯 세션 요청은 502/422로 실패합니다). 빠르게 응답하세요. 결정을 내리는 동안
발신자는 연결음을 듣고 있습니다.
요청 페이로드
전화 통화(telephony.incoming)의 경우:
{
"type": "telephony.incoming",
"data": {
"call_id": 987654321,
"from_number": "+14155550199",
"to_number": "+15551234567"
}
}| 필드 | 유형 | 설명 |
|---|---|---|
call_id | integer | 통화 ID — 이 통화의 모든 이벤트에서 일관되게 유지됩니다 |
from_number | string | E.164 발신자 번호 |
to_number | string | E.164 대상 번호(ThunderPhone 번호 중 하나) |
웹 위젯 세션(web.incoming)의 경우 data는 전화번호 대신
삽입된 페이지를 식별합니다.
{
"type": "web.incoming",
"data": {
"call_id": 987654322,
"origin_domain": "https://example.com",
"publishable_key_prefix": "pk_live_a1b2"
}
}| 필드 | 유형 | 설명 |
|---|---|---|
call_id | integer | 통화 ID |
origin_domain | string | 위젯을 호스팅하는 페이지 오리진 |
publishable_key_prefix | string | 세션을 연 공개 키의 처음 몇 글자 |
language, primary_language | string | 위젯 세션에서 언어 재정의를 요청한 경우 포함됩니다 |
voice | string | 위젯 세션에서 음성 재정의를 요청한 경우 포함됩니다 |
website_context | string | 위젯이 세션별 페이지 컨텍스트를 전달한 경우 포함됩니다 |
응답 스키마
이 통화의 에이전트 구성을 설명하는 JSON 객체를 반환합니다.
prompt 및 voice는 필수이며, 그 외의 모든 항목은 선택 사항입니다.
{
"prompt": "You are a helpful booking assistant for Acme Restaurant.",
"voice": "john",
"product": "spark",
"background_track": null,
"tools": []
}| 필드 | 유형 | 필수 | 설명 |
|---|---|---|---|
prompt | string | 예 | 에이전트를 구동하는 시스템 프롬프트 |
voice | string | 예 | GET /v1/voices의 음성 ID(예: john)입니다. voice_name도 별칭으로 허용됩니다. 알 수 없는 음성은 유효성 검사에 실패하며 통화가 거부됩니다 |
product | string | 아니요 | 기본값은 spark입니다. 허용 값: spark, bolt, storm-base, storm-base-with-ack, storm-extra, storm-extra-with-ack |
thinking_level | string | 아니요 | minimal, base(기본값) 또는 extra입니다. Storm 제품에서는 재정의됩니다. storm-extra*는 extra를 강제하고, 그 외 storm-*는 base를 강제합니다 |
audio_context_mode | string | 아니요 | full(기본값) 또는 reduced |
watchdog_enabled | boolean | 아니요 | 이 통화에 대한 감독을 활성화합니다. 기본값은 false입니다 |
additional_audio_context | boolean | null | 아니요 | 가장 최근 턴만 포함하는 대신 발신자 오디오의 최근 몇 턴을 포함하여, 적은 지연 시간 및 비용 증가로 수정과 철자/숫자 중심 데이터 수집을 개선합니다. 인바운드 세션에서는 기본적으로 활성화되고 아웃바운드 전화 통화에서는 비활성화됩니다. null은 기본값을 유지합니다 |
storm_feedback_mode | string | 아니요 | none, acknowledgement(기본값) 또는 tick |
language | string | 아니요 | primary_language의 축약형 |
primary_language | string | 아니요 | 정규화된 언어 코드(기본값 en)입니다. 확인할 수 없는 코드는 통화를 거부합니다 |
has_additional_languages | boolean | 아니요 | 기본값은 false입니다 |
additional_languages | array of string | 아니요 | 에이전트가 전환할 수 있는 추가 언어 |
native_voice_switching | boolean | 아니요 | 기본값은 false입니다. 통화가 다른 언어로 전환되면 구성된 음성을 유지하는 대신 해당 언어의 원어민 음성으로 전환합니다(성별 일치) |
background_track | string | null | 아니요 | 배경 오디오 ID 또는 null |
acknowledgement_prompt_mode | string | 아니요 | auto(기본값) 또는 manual(Storm-with-ack 제품) |
acknowledgement_prompt | string | 아니요 | acknowledgement_prompt_mode="manual"일 때 사용됩니다 |
silence_interval_seconds | integer | null | 아니요 | 5~120. 확인 응답 전 발신자가 침묵한 시간(초) |
silence_max_checkins | integer | null | 아니요 | 1~10 |
silence_checkins_enabled | boolean | 아니요 | 기본값은 true입니다 |
connect_tone_enabled | boolean | 아니요 | 기본값은 false입니다 |
voicemail_action | string | 아니요 | prompt(기본값), hangup 또는 message |
voicemail_message | string | 아니요 | voicemail_action="message"일 때 사용됩니다 |
agent_name | string | 아니요 | 대시보드 및 위젯에 표시되는 이름 |
org_name | string | 아니요 | 에이전트 페르소나의 조직 표시 이름 |
tools | array | 아니요 | 인라인 함수 도구 스키마(함수 도구 참조) |
call_id | integer | 아니요 | 요청의 통화 ID를 선택적으로 에코합니다. 무시됩니다 |
prompt 및 voice는 필수이므로 {} 또는 유효성 검사에 실패하는 응답을 반환하면
통화가 422와 함께 거부됩니다. 이 경로에는 정적 에이전트 대체가 없습니다(웹훅 모드의
번호 또는 키에는 할당된 에이전트가 없습니다).
응답 크기 제한
핸들러 예시
import hashlib
import hmac
import json
import os
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
WEBHOOK_SECRET = os.environ["THUNDERPHONE_WEBHOOK_SECRET"]
def verify(body: bytes, signature: str) -> bool:
expected = hmac.new(WEBHOOK_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"] == "telephony.incoming":
caller = event["data"]["from_number"]
prompt = (
"Greet the caller as a San Francisco local…"
if caller.startswith("+1415")
else "You are a friendly customer support agent…"
)
return {
"prompt": prompt,
"voice": "john",
"product": "spark",
}
if event["type"] == "web.incoming":
return {
"prompt": "You are the website's helpful voice assistant…",
"voice": "john",
"product": "spark",
}
return {}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" }),
(req, res) => {
if (!verify(req.body, req.header("X-ThunderPhone-Signature"))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
if (event.type === "telephony.incoming" || event.type === "web.incoming") {
const caller = event.data.from_number || "web";
const prompt = caller.startsWith("+1415")
? "Greet the caller as a San Francisco local…"
: "You are a friendly customer support agent…";
return res.json({
prompt,
voice: "john",
product: "spark",
});
}
res.json({});
},
);함수 도구가 포함된 응답
대화 중 AI가 API를 호출할 수 있도록 도구를 연결하세요.
{
"prompt": "You are a booking assistant. Use the available tools to help customers schedule appointments.",
"voice": "john",
"product": "spark",
"tools": [
{
"type": "function",
"function": {
"name": "search_appointments",
"description": "Find available appointment slots",
"parameters": {
"type": "object",
"properties": {
"date": { "type": "string", "description": "YYYY-MM-DD" },
"service": { "type": "string" }
},
"required": ["date"]
}
},
"endpoint": {
"url": "https://api.example.com/appointments/search",
"method": "POST",
"headers": {
"X-Api-Key": "your-key"
}
}
}
]
}제품 티어 요약
| 제품 | 지연 시간 | 추론 | 응답 확인 |
|---|---|---|---|
spark | 최저 | 기본 | — |
bolt | 낮음 | 향상됨 | — |
storm-base | 중간 | 강력함 | — |
storm-base-with-ack | 중간 | 강력함 | 사고 중 자동 채움 응답 |
storm-extra | 높음 | 심층 | — |
storm-extra-with-ack | 높음 | 심층 | 사고 중 자동 채움 응답 |