View as Markdown
Open in
telephony.complete / web.complete
通話終了時に、文字起こし、録音URL、指標とともに配信されるノンブロッキングWebhook。
すべての通話終了後に完了イベントが発生します。対象は着信電話、 発信電話、Web 通話、テスト通話(ビルダーのマイクセッション)です。これは ノンブロッキングです。任意の 2xx で応答してください。
イベントは両方のパスで配信されます。
- Webhook エンドポイント は、
以下に記載する安定したペイロード、配信ごとの
event_id、30 秒のタイムアウト、 および最大 24 時間の再試行とともに、telephony.complete(電話通話)またはweb.complete(Web 通話および ビルダーのマイクテスト通話)を受信します。 - レガシーの単一 URL Webhook は、 やや異なるペイロードで 1 回の同期試行を受信します(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",
"extracted_data": {
"status": "completed",
"fields": {
"customer_name": "Alex Morgan",
"appointment_date": "2026-04-23"
},
"evidence": {
"customer_name": {
"quote": "My name is Alex Morgan",
"speaker_role": "caller",
"turn_index": 4
},
"appointment_date": {
"quote": "April 23 works for me",
"speaker_role": "caller",
"turn_index": 7
}
},
"verification": "verified",
"field_reasons": {},
"schema_version": "92850758e231a3c95a..."
},
"from_number": "+14155550199",
"product": "spark",
"recording_stopped_at": null,
"recording_stopped_reason": null,
"recording_url": "https://storage.example.com/…",
"start_time": "2026-04-20T18:24:10.113Z",
"status": "completed",
"summary": "Alex Morgan called to book a follow-up visit and confirmed Thursday, April 23. The agent read the appointment back and Alex asked for a reminder text the day before.",
"to_number": "+15551234567",
"transcripts": [ /* see Transcript format */ ],
"transfer_number": null,
"unresolved_variables": ["campaign_owner"],
"variables": {"campaign_name": "Spring renewals"},
"voice": "john"
},
"event_id": "6a7b8c9d-0e1f-4a2b-8c3d-4e5f6a7b8c9d",
"type": "telephony.complete"
}| フィールド | 型 | 説明 |
|---|---|---|
call_id | 整数 | この通話のすべてのイベントで共通の安定した ID |
agent_id | 整数 | null | 割り当てられていた場合に通話を処理したエージェント |
agent_name | 文字列 | null | 割り当てられていた場合に通話を処理したエージェント |
direction | 文字列 | inbound、outbound、web、test。過去のペイロードには旧式の mic または widget 値が含まれる場合があります |
from_number, to_number | 文字列 | E.164。Web 通話およびテスト通話では、from_number は文字列リテラルの "web" です |
origin_domain | 文字列 | Web/テストのみ — ウィジェットをホストしたページのオリジン(mic セッションでは空) |
start_time, end_time | タイムスタンプ | ISO 8601 UTC |
duration_seconds | 整数 | null | 開始時刻と終了時刻から算出 |
status | 文字列 | completed または failed |
end_reason | 文字列 | 下の表を参照 |
product, voice | 文字列 | 通話時に有効だったエージェント設定 |
variables | オブジェクト | 通話開始時にスナップショットされた入力変数 |
unresolved_variables | 配列 | 通話設定で参照されているが、通話開始時に提供されなかった変数名 |
transfer_number | 文字列 | null | 通話が転送された場合に設定 |
recording_url | 文字列 | null | 有効期限付き署名 URL。速やかにダウンロードしてください。録音アーティファクトが利用できない場合は null |
recording_stopped_at | タイムスタンプ | null | 通話終了前に録音が停止した時刻。この時点以降の内容は録音に含まれません。録音が通話終了まで継続した場合、または通話が録音されなかった場合は null |
recording_stopped_reason | 文字列 | null | 録音が早期に停止した理由。warm_transfer の場合、ウォーム転送が転送先へ発信する直前に録音が停止し、転送に失敗して発信者がエージェントに戻った場合も停止したままです。その他の値は参考情報として扱ってください。今後、値が追加される場合があります。recording_stopped_at が null の場合は null |
billable_minutes | 数値 | 請求対象の分数。最も近い 4 分の 1 分に丸められます(15 秒単位、最小 0.25)。留守番電話への直接接続通話でも実際の計測分数がここに報告されますが、料金はプランレートで 1 分分に上限設定されます。 |
billing_total_cents | 整数 | 米ドルのセント |
transcripts | 配列 | ターンごとの文字起こしエントリ。文字起こしを利用できない場合は空になることがあります |
summary | 文字列 | null | エージェントビルダーの 収集するデータ で 通話サマリーを生成 を有効にし(Agent の call_summary_enabled)、生成に成功した場合の、平易な言葉による通話後サマリーです。2~4 文の文章で最大 2,000 文字とし、通話の言語で記述されます。通話の言語は、通話による上書きがない限りエージェントの主要言語です。サマリーには短すぎて要約できない通話、文字起こしがない通話、録音が無効なエージェントの通話も含め、それ以外の場合は null です。サマリーは抽出と同じ通話後ステップで生成されるため、通話完了 Webhook を送信する前に抽出データを待機 設定(extraction_blocks_completion)によって、このイベントがサマリーを待機するかどうかが決まります。待機モードでは、このイベントが送信される前にサマリーが準備されます(サマリー自体には、ステップの 75 秒の予算内で最大 30 秒が割り当てられます)。イベントが待機しない場合(never、または except_transfers での転送)、このフィールドは null となり、サマリーは call.data_extracted で再送信されません。後で Call オブジェクトの summary から読み取ってください。ダッシュボードの通話ページでも利用できます |
extracted_data | オブジェクト | null | status、fields、evidence、verification、field_reasons、schema_version を含む構造化抽出結果。null ではない各フィールドには、構造的に正確性を確認した完全一致の引用(最大 1,000 文字)と、発話者ロールおよびターンインデックスが含まれます。モデルが返した引用が長すぎる場合は切り詰めずに拒否されます。フィールドが null の場合、エビデンスは null です。verified は、すべての候補が有効な独立した判定をちょうど 1 件受け取ったことを意味します。unavailable には、検証器の不正または部分的な出力も含まれます。有効な部分判定は引き続き適用されますが、有効な判定が 1 件もない候補は null 化されます。status は completed、failed、exhausted、skipped、または skipped_recording_disabled です。エージェントに抽出フィールドがない場合は null |
steps_trace | 配列 | エージェントにステップまたはハンドオフがある通話でのみ利用できます。通話がそれらを移動した順序です。各エントリには、t_ms(通話開始からのミリ秒数)、agent(エージェントのキー。a に ID を加えたもの)、step(ステップ ID。ステップのないエージェントへのハンドオフでは null)、および via(start、model、condition、on_enter、または handoff)が含まれます。ID と時刻のみを保持するため、録音が無効でも保持されます |
collected_values | オブジェクト | エージェントにステップまたはハンドオフがある通話でのみ利用できます。エージェントがステップ内で収集した値を収集フィールド名でキー付けしたものに加え、各ステップの on_enter_tool 結果に対する <tool>_result を含みます。通話コンテンツが保持されない場合(録音が無効)は省略されます |
終了理由
| 値 | 意味 |
|---|---|
user_hangup | 相手側が先に通話を終了しました |
ai_hangup | AIが意図的に通話を終了しました |
ai_transfer | AIが通話を転送しました。transfer_number が設定されています |
ai_warm_transfer | AIがウォーム転送(担当者を介した転送)を完了しました |
voicemail_hangup | ボイスメールが検出され、voicemail_action に従って通話が終了しました |
max_duration | 通話が最大時間制限に達しました。エージェントまたは通話自体の制限、あるいはプラットフォームの6時間制限です。時間制限の警告後にAIまたは発信者が終了した通話では、ai_hangup または user_hangup が維持されます |
superseded | セッションが新しいセッションに置き換えられました |
inbound_config_webhook_failed | Webhookモードの番号への着信通話です。設定Webhook が使用可能な設定を返さなかったため、エージェントが応答する前に通話が終了しました。status は failed で、この通話は課金されません |
unknown | 終了理由を特定できませんでした |
文字起こし形式
transcripts の各エントリは、1つの会話ターンです。ロールは
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 | string | user、model、tool、または system |
content_type | string | 発話には text/plain、ツール呼び出し、ツール結果、システムイベントには application/json |
content | string | object | 発話テキスト、または上記の構造化オブジェクト。ツール呼び出し: {"tool_call": name, "arguments": {…}}。ツール結果: {"tool_name": name, "response": {…}} |
start_ms, end_ms | integer | 通話開始からのオフセット(ミリ秒)。音声タイミングが判明している場合に存在します |
ttfa_ms | integer | 測定されている場合の model ターンの最初の音声までの時間 |
audio_url, audio_urls | string / array | ターンごとに録音された場合の、そのターンの音声用期限付き署名 URL |
完全に構造化されたターン履歴(割り込みマーカー、
確認プロンプト、生の位置情報を含む)については、
GET /v1/calls/{call_id}/history を使用します。
レガシーペイロードの違い
レガシーの単一 URL Webhook エンベロープは
{"type": "telephony.complete" | "web.complete", "data": {…}} で、
event_id はありません。また、その data はエンドポイントのペイロードとは異なります。
レガシーの完了ペイロードには、agent_id と agent_name も含まれます。
- ターン配列は
transcriptsではなくhistoryにあります(ターンスキーマは上記と同じです)。 - フィールドセットは未加工の通話終了レポートであり、上記の表以外の追加の内部フィールドを含む場合があります。未知のフィールドは参考情報として扱ってください。
- Web 通話(
direction: "web")ではfrom_number/to_numberが 省略 され、origin_domainが追加されます。 - Builder のマイクテスト通話は、レガシーパスでは
telephony.completeとしてレポートされます(エンドポイントシステムではweb.completeにマッピングされます)。 - 転送の調整: 通話が転送中に終了した場合、レガシー Webhook は同期的に呼び出され、転送先が準備できていないことを示すために
{"transfer_ready": false}を返すことがあります。その他の応答(またはレガシー Webhook がない場合)では転送が続行されます。この目的でエンドポイント配信が参照されることはありません。
ハンドラーの例
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ツールで開くか、独自の評価モデルで処理します。
通知
転送または失敗時にチームメンバーへ通知します。