通話後ウェブフック
call.completedイベントを購読 — 文字起こし、録音、結果。
通話後Webhook
Finnの通話が終了するたびに、自社サーバーへ通知します。文字起こし、録音URL、構造化された結果、感情分析、コストを受け取れます。通話データをCRM、分析基盤、業務パイプラインに同期するうえで最も重要なWebhookです。
発火するタイミング
- イベント:
call.completed - 発火: 通話終了から5秒以内(応答、不在、留守番電話、話中、失敗など理由を問わず)
- 順序: ベストエフォート。
created_atと冪等なハンドラを使用し、厳密な順序に依存しないでください。 - 再試行:
5xxまたはタイムアウト時に約10分間で5回。最終試行はダッシュボードのWebhookログに記録されます。
サブスクリプションの登録
ダッシュボードから
設定 → 連携 → Webhook → 新規Webhook。
イベント一覧からcall.completedを選択し、エンドポイントURLを貼り付けて保存します。保存すると署名シークレットが一度だけ表示されます。再表示はできないため、その場でコピーしてください。
APIから
curl https://api.hirefinn.ai/v1/webhooks \
-H "Authorization: Bearer $FINN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/finn/post-call",
"events": ["call.completed"],
"description": "CRM + analytics sync"
}'
レスポンスにはsecretが含まれます。安全に保管してください。
ペイロード
{
"id": "evt_2H4abc",
"type": "call.completed",
"created_at": "2026-05-22T14:32:14.892Z",
"org_id": "org_8fac17c5",
"data": {
"call_id": "cal_xyz789",
"deployment_id": "dep_abc123",
"finn_id": "fn_def456",
"phone_number_id": "ph_1234",
"from": "+919876543210",
"to": "+918765432100",
"call_type": "outbound",
"channel": "pstn",
"started_at": "2026-05-22T14:30:01.245Z",
"answered_at": "2026-05-22T14:30:04.812Z",
"ended_at": "2026-05-22T14:32:14.123Z",
"duration_seconds": 130,
"ring_seconds": 4,
"outcome": {
"label": "qualified",
"confidence": 0.87,
"extracted_fields": {
"preferred_slot": "2026-05-24T15:00:00+05:30",
"budget": "25000",
"is_decision_maker": true
}
},
"sentiment": "positive",
"csat_score": null,
"call_status": "completed",
"hangup_party": "agent",
"recording_url": "https://recordings.hirefinn.ai/.../cal_xyz789.mp3",
"recording_duration_seconds": 130,
"transcript_url": "https://transcripts.hirefinn.ai/.../cal_xyz789.json",
"credits_charged": 3,
"currency": "INR",
"monetary_value": 10.05,
"audience_id": "aud_xyz789",
"audience_row": {
"name": "Priya M",
"phone": "+919876543210",
"loan_amount": "500000"
},
"metadata": {
"campaign_tag": "may-cohort-3"
}
}
}
フィールドリファレンス
| フィールド | 型 | 備考 |
|---|---|---|
call_id | string | グローバルに一意。重複排除キーとして使用します。 |
deployment_id | string | 発信元キャンペーン。単発のテスト通話ではnull。 |
from / to | E.164 | 変換前の値。WhatsApp通話ではここにwa_idが入ります。 |
channel | enum | pstn | whatsapp | sip |
started_at | ISO-8601 | Finnが発信を開始、または着信を受けた時刻 |
answered_at | ISO-8601 / null | 通話が応答されなかった場合はnull |
duration_seconds | int | 課金対象の通話時間(answered_at から ended_at まで) |
outcome.label | string | プロンプトで定義したカスタムの結果分類 |
outcome.confidence | float 0–1 | ラベルに対するモデルの確信度 |
outcome.extracted_fields | object | 通話ごとの構造化データ(プロンプト設計に応じた自由形式) |
sentiment | enum | positive | neutral | negative |
call_status | enum | completed | no_answer | busy | failed | voicemail |
hangup_party | enum | caller | agent | system |
credits_charged | float | この通話に対するウォレットからの引き落とし額 |
audience_row | object | 発信でダイヤルした CSV 行の全体。着信の場合は null。 |
署名の検証
Finn はすべての Webhook を HMAC-SHA256 で署名します。ペイロードを信頼する前に検証してください。
ヘッダー: X-Finn-Signature: t=1716391823,v1=abc123...
t= はタイムスタンプです。v1= は HMAC-SHA256(secret, t + "." + raw_body) です。
Node
import crypto from "crypto";
function verifyFinnSignature(
rawBody: string,
header: string,
secret: string,
toleranceSeconds = 300,
): boolean {
const parts = Object.fromEntries(
header.split(",").map((p) => p.split("=") as [string, string]),
);
const ts = parseInt(parts.t, 10);
const sig = parts.v1;
if (!ts || !sig) return false;
if (Math.abs(Date.now() / 1000 - ts) > toleranceSeconds) return false; // replay guard
const expected = crypto
.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}
Python
import hmac, hashlib, time
def verify_finn_signature(raw_body: bytes, header: str, secret: str, tolerance=300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
ts = int(parts.get("t", 0))
sig = parts.get("v1", "")
if not ts or not sig: return False
if abs(time.time() - ts) > tolerance: return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
必ず生のリクエストボディで検証してください。パース済みの JSON では、再シリアライズによって空白が変わり HMAC が一致しなくなります。
ペイロードの処理(Node の例)
import express from "express";
const app = express();
// raw body required for signature verification
app.post("/finn/post-call",
express.raw({ type: "application/json" }),
async (req, res) => {
const sig = req.headers["x-finn-signature"] as string;
const ok = verifyFinnSignature(req.body.toString(), sig, process.env.FINN_WEBHOOK_SECRET!);
if (!ok) return res.status(401).send("bad signature");
const event = JSON.parse(req.body.toString());
// Dedupe — idempotent processing
if (await db.eventExists(event.id)) {
return res.status(200).send("dup");
}
await db.markEventSeen(event.id);
// Route by event type
if (event.type === "call.completed") {
await handleCallCompleted(event.data);
}
res.status(200).send("ok");
},
);
async function handleCallCompleted(call: any) {
// 1. Update CRM record
await crm.updateLead(call.audience_row?.phone, {
last_call_outcome: call.outcome.label,
last_call_sentiment: call.sentiment,
last_call_recording: call.recording_url,
});
// 2. If qualified, fire a Slack alert
if (call.outcome.label === "qualified") {
await slack.notify("#sales-hot-leads", `Hot lead: ${call.audience_row.name}`);
}
// 3. Push to data warehouse
await warehouse.insert("finn_calls", call);
}
信頼性と冪等性
event.idで重複排除する — エンドポイントがタイムアウトした場合、Finn は配信済みのイベントを再送することがあります- 5 秒以内に 2xx を返す — 返さない場合は失敗と見なして再送します
- 重い処理は非同期で — ペイロードをキューに入れ、すぐに ack を返す
- ダッシュボードから再送 — 配信失敗は Settings → Integrations → Webhooks → Logs に「Replay」ボタン付きで表示されます
よくある落とし穴
| 症状 | 対処 |
|---|---|
| 署名検証が必ず失敗する | 検証前に JSON をパースしています。まず生のボディを検証してください。 |
| CRM に重複行ができる | event.id で重複排除していません。ユニーク制約または処理済みセットを追加してください。 |
| イベントの遅延 | エンドポイントの応答が 5 秒を超えました。処理をキューに移してください。 |
audience_row が欠落している | インバウンド通話でした(オーディエンスなし)。まず call_type を確認してください。 |
| 録音 URL が 403 | 録音 URL は既定で 30 日後に失効します。長期保管には自社ストレージへ複製してください。 |
関連イベント
通話後 webhook は複数あるうちの 1 つです。全一覧はこちら:
| イベント | 発生タイミング |
|---|---|
call.started | キャリアが応答したとき |
call.completed | 本ページ |
call.transferred | オペレーターへの温かい転送 |
deployment.completed | キャンペーンがオーディエンスを使い切ったとき |
wallet.low_balance | 設定したしきい値を下回ったとき |
関連情報
Was this page helpful?
Still stuck or have feedback?
Email [email protected] or use the chat bubble in the bottom-right corner — it's a Finn that knows the Academy cold.