Open in
telephony.incoming / web.incoming
수신 통화의 구성을 실시간으로 설정하는 차단 웹훅입니다.
수신 전화가 할당된 에이전트가 없는
번호에 도달하거나, 웹 위젯 세션이 mode="webhook"의 공개 가능 키에서 시작되면 ThunderPhone은
레거시 웹훅 URL로 차단
telephony.incoming / web.incoming 요청을 전송하고 구성 응답을 기다립니다. 전화의 경우 재시도 1회를 포함하여 최대 13초,
위젯 세션의 경우 최대 10초 동안 기다립니다(시간 초과 및 재시도 참조). 이 교환을 사용하여 통화별로 프롬프트, 음성 및 도구를 동적으로 선택합니다.
전체 패턴은 동적 통화 구성 가이드를 참조하세요.
시간 초과 및 재시도
신속히 응답하세요. 결정하는 동안 발신자는 회선에서 기다리고 있습니다.
전화 통화(telephony.incoming)의 경우:
- 첫 번째 시도는 최대 10초까지 걸릴 수 있습니다.
- 시간 초과, 연결 실패 또는
5xx,408,429응답이 발생하면 ThunderPhone이 한 번 더 시도합니다. 재시도에는 전체 교환에 대한 13초 예산에서 남은 시간이 할당됩니다. 첫 번째 시도가 시간 초과된 후에는 약 3초, 빠르게 실패한 경우에는 거의 13초가 남습니다. 남은 시간이 2초 미만이면 재시도하지 않습니다. - 그 외
4xx응답, 크기 제한을 초과하는 응답 또는 본문이 유효한 구성이 아닌2xx응답은 최종 응답으로 처리되며 재시도하지 않습니다. - 두 시도 모두 동일한 본문, 동일한
X-ThunderPhone-Signature, 동일한X-ThunderPhone-Idempotency-Key헤더 (telephony-incoming-<call_id>)를 전송하므로, 부작용이 있는 핸들러도 재시도를 인식할 수 있습니다.
대체 에이전트는 없습니다. 사용 가능한 구성이 도착하지 않으면 발신자를 무음 상태의
회선에 남겨 두지 않고 통화를 즉시 종료합니다. 통화 기록에는 청구되지 않은 실패 통화로
표시되며 end_reason은 inbound_config_webhook_failed이고, 일반적인
telephony.complete 이벤트가 전송됩니다.
위젯 세션(web.incoming)에는 최대 10초의 단일 시도가 제공됩니다. 실패하면 세션 요청은
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 | 정수 | 통화 ID |
origin_domain | 문자열 | 브라우저가 전송한 위젯을 호스팅하는 페이지의 출처입니다. 브라우저가 아닌 클라이언트는 어떤 값이든 보낼 수 있으므로, 요청의 출처를 증명하는 것으로 취급하지 마십시오 |
publishable_key_prefix | 문자열 | 세션을 연 공개 키의 처음 몇 글자입니다 |
language, primary_language | 문자열 | 위젯 세션에서 언어 재정의를 요청한 경우 존재합니다 |
voice | 문자열 | 위젯 세션에서 음성 재정의를 요청한 경우 존재합니다 |
website_context | 문자열 | 위젯이 세션별 페이지 컨텍스트를 전달한 경우 존재합니다 |
재정의 필드(language, voice, website_context 및 모든 브라우저 variables)는 공개 키의 브라우저 재정의에서 허용하는 경우에만 전달됩니다. 키가 허용하지 않는 필드를 포함한 세션 요청은 이 웹훅이 호출되기 전에 거부됩니다.
응답 스키마
이 통화의 에이전트 구성을 설명하는 JSON 객체를 반환합니다.
prompt 및 voice는 필수이며, 그 외의 모든 항목은 선택 사항입니다.
{
"prompt": "You are a helpful booking assistant for Acme Restaurant.",
"voice": "john",
"product": "spark",
"background_track": null,
"tools": []
}| 필드 | 유형 | 필수 | 설명 |
|---|---|---|---|
prompt | 문자열 | 예 | 에이전트를 구동하는 시스템 프롬프트 |
voice | 문자열 | 예 | GET /v1/voices의 음성 ID(예: john)입니다. voice_name은 별칭으로 허용됩니다. 알 수 없는 음성은 유효성 검사에 실패하며 통화가 거부됩니다. |
product | 문자열 | 아니요 | 기본값은 spark입니다. 허용되는 값: spark, bolt, storm-base, storm-base-with-ack, storm-extra, storm-extra-with-ack |
thinking_level | 문자열 | 아니요 | minimal, base(기본값) 또는 extra입니다. Storm 제품에서는 재정의됩니다. storm-extra*는 extra를 강제하고, 다른 storm-*는 base를 강제합니다. |
audio_context_mode | 문자열 | 아니요 | full(기본값) 또는 reduced |
watchdog_enabled | 불리언 | 아니요 | 이 통화에 대한 감독을 활성화합니다. 기본값은 false입니다. |
additional_audio_context | 불리언 | null | 아니요 | 가장 최근 턴만 포함하는 대신 발신자 오디오의 최근 몇 개 턴을 포함하여, 약간의 지연 시간 및 비용 증가로 수정과 철자/숫자가 많은 데이터 수집을 개선합니다. 수신 세션에서는 기본적으로 켜지고 발신 전화 통화에서는 꺼집니다. null은 기본값을 유지합니다. |
storm_feedback_mode | 문자열 | 아니요 | none, acknowledgement(기본값) 또는 tick |
language | 문자열 | 아니요 | primary_language의 축약형 |
primary_language | 문자열 | 아니요 | 정규화된 언어 코드(기본값 en)입니다. 확인할 수 없는 코드는 통화를 거부합니다. |
has_additional_languages | 불리언 | 아니요 | 기본값은 false입니다. |
additional_languages | 문자열 배열 | 아니요 | 에이전트가 전환할 수 있는 추가 언어 |
native_voice_switching | 불리언 | 아니요 | 기본값은 false입니다. 통화가 다른 언어로 전환될 때 구성된 음성을 유지하는 대신 해당 언어의 원어민 음성으로 교체합니다(성별 일치). |
language_switching | 문자열 | null | 아니요 | ask_first(기본값) 또는 follow_caller입니다. null은 ask_first와 같습니다. 다국어 에이전트가 상대방이 지원 언어 중 다른 언어를 사용할 때 처리하는 방식입니다. ask_first는 상대방이 해당 언어를 요청하거나 그 언어로 완전한 문장을 말하면 즉시 전환하고, "예"처럼 짧은 발화 뒤에는 해당 언어로 계속할지 묻습니다. follow_caller는 한 단어만 말해도 상대방이 사용하는 구성된 언어로 전환합니다. additional_languages가 설정된 경우에만 적용됩니다. |
background_track | 문자열 | null | 아니요 | 배경 오디오 ID 또는 null |
acknowledgement_prompt_mode | 문자열 | 아니요 | auto(기본값) 또는 manual(Storm-with-ack 제품) |
acknowledgement_prompt | 문자열 | 아니요 | acknowledgement_prompt_mode="manual"일 때 사용됩니다. |
silence_interval_seconds | 정수 | null | 아니요 | 5–120입니다. 확인 안내 전 발신자 무음 시간(초)입니다. |
silence_max_checkins | 정수 | null | 아니요 | 1–10 |
silence_checkins_enabled | 불리언 | 아니요 | 기본값은 true입니다. |
connect_tone_enabled | 불리언 | 아니요 | 기본값은 false입니다. |
pronunciation_dicts | 불리언 | null | 아니요 | 기본값은 false이며, null은 false와 같습니다. true이면 통화의 Cartesia 음성은 해당 언어용 ThunderPhone 발음 사전이 있는 경우 이를 사용합니다. 전화 및 위젯 통화에서 작동합니다. 자체 Cartesia API 키의 음성이 아닌 ThunderPhone 자체 Cartesia 음성에만 적용됩니다. 저장된 에이전트 설정이 적용되는 agent_id와 함께 사용할 때는 사용되지 않습니다. |
voicemail_action | 문자열 | 아니요 | prompt(기본값), hangup 또는 message |
voicemail_message | 문자열 | 아니요 | voicemail_action="message"일 때 사용됩니다. |
agent_name | 문자열 | 아니요 | 대시보드와 위젯에 표시되는 이름 |
org_name | 문자열 | 아니요 | 에이전트 페르소나의 조직 표시 이름 |
tools | 배열 | 아니요 | 인라인 함수 도구 스키마(함수 도구 참조) |
transfer_destinations | 객체 배열 | null | 아니요 | 이 통화에서 전환할 수 있는 최대 50개의 {name, number} 항목입니다. 에이전트의 전환 번호와 동일하게 유효성을 검사합니다. 에이전트는 이름으로, 그리고 이 번호로만 전환합니다. []는 전환이 없음을 의미합니다. 에이전트가 선택한 번호로 전환하도록 하려면 이를 생략하거나 null을 전송합니다. |
timezone | 문자열 | 아니요 | America/Chicago와 같은 이 통화의 IANA 시간대입니다. 에이전트는 자체 시간대보다 이를 우선하여 날짜와 시간을 말합니다. agent_id와 함께 사용할 수도 있습니다. IANA 시간대가 아닌 값은 무시됩니다. 통화별 시간대 설정을 참조하세요. |
call_id | 정수 | 아니요 | 요청의 통화 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 | 높음 | 심층 | 사고 중 자동 채움 응답 |