ThunderPhone 2.0을 출시했습니다.셀프서비스로, 분당 2¢부터.출시 소식 보기

Webhooks

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_idinteger통화 ID — 이 통화의 모든 이벤트에서 일관되게 유지됩니다
from_numberstringE.164 발신자 번호
to_numberstringE.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로 실패합니다. 이 경로에서는 정적 에이전트 대체가 제공되지 않습니다(웹훅 모드의 번호 또는 키에는 할당된 에이전트가 없습니다).


응답 크기 제한


핸들러 예시

Python (FastAPI)
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 {}
Node.js (Express)
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높음심층사고 중 자동 채움 응답

관련 항목