Inbound Webhooks
Dynamic agent routing — Finn calls your server when an inbound call lands.
Inbound Webhooks
When an inbound call arrives, Finn can ask your server in real time which agent to answer with, what variables to inject, and what knowledge base to load. This lets you route dynamically — by caller number, account tier, time of day, A/B cohort, or any logic your backend cares about.
If a fixed agent is enough, skip this. If you want dynamic per-call routing, this is the page.
How it works
caller dials → Finn answers ring → Finn POSTs to your URL → your server returns agent + vars → Finn streams the voice agent
The handshake adds ~100–200ms of latency. Your endpoint must respond in under 500ms or Finn falls back to the default agent.
Configure
1. Set the inbound webhook URL
Dashboard → Settings → Phone Numbers → [number] → Inbound Webhook URL.
Paste your HTTPS endpoint. Save. Finn pings it once with a {"ping": true} body to verify reachability.
Via API
curl https://api.hirefinn.ai/v1/phone-numbers/ph_1234 \
-X PATCH \
-H "Authorization: Bearer $FINN_API_KEY" \
-d '{ "inbound_webhook_url": "https://your-app.com/finn/inbound" }'
2. Set a fallback agent
Configure a default Finn for that number — used if your webhook times out, errors, or returns an invalid response.
Request payload
Finn POSTs to your URL with:
{
"session_id": "ws_2H4abc",
"request_type": "inbound",
"phone_number_id": "ph_1234",
"channel": "pstn",
"from": {
"phone": "+919876543210",
"country": "IN",
"carrier": "Airtel"
},
"to": {
"phone": "+918765432100",
"country": "IN"
},
"received_at": "2026-05-22T14:30:00Z",
"metadata": {}
}
For WhatsApp calls, the from block uses WhatsApp-specific fields:
"from": {
"wa_id": "919876543210",
"display_name": "Priya M",
"profile_pic_url": "https://..."
},
"channel": "whatsapp"
Response
Return a JSON body within 500ms describing how to route the call:
{
"finn_id": "fn_def456",
"language": "hi-IN",
"variables": {
"customer_name": "Priya M",
"account_tier": "premium",
"last_order_id": "ord_99831",
"preferred_agent": "Aria"
},
"knowledge_base_ids": ["kb_general", "kb_premium_perks"],
"metadata": {
"campaign_tag": "premium-support-q2",
"ab_cohort": "B"
},
"recording_enabled": true,
"max_call_duration_seconds": 600
}
Field reference
| Field | Required? | Notes |
|---|---|---|
finn_id | Yes | Which agent answers. Must belong to your org. |
language | No | Override agent's default language. en-US, hi-IN, es-ES, etc. |
variables | No | Free-form key/value bag. Available inside the prompt as {variable_name}. |
knowledge_base_ids | No | Override which KBs are loaded for this call. |
metadata | No | Attached to the call record and post-call webhook. Use for your own analytics tagging. |
recording_enabled | No | Override the agent default. |
max_call_duration_seconds | No | Hard cap. Default is 1800 (30 min). |
Reject the call
{ "action": "reject", "reason": "blocked_caller" }
Finn ends the call with a configured outgoing message ("This number is no longer in service"). Use for DNC enforcement, blocked accounts, or after-hours hangups.
Transfer immediately
{ "action": "transfer", "to": "+918888888888", "reason": "vip_route" }
Skip the AI agent entirely — route directly to a human. Useful for VIP / escalation tiers.
Implementation example (Node)
import express from "express";
const app = express();
app.use(express.json());
app.post("/finn/inbound", async (req, res) => {
const { from, to, channel } = req.body;
// 1. Look up caller in CRM (must be fast — DB index by phone)
const phone = from.phone ?? `+${from.wa_id}`;
const customer = await crm.findByPhone(phone);
// 2. Block known DNC
if (customer?.dnc) {
return res.json({ action: "reject", reason: "dnc" });
}
// 3. VIP → straight to human
if (customer?.tier === "vip") {
return res.json({
action: "transfer",
to: process.env.VIP_DESK_NUMBER!,
reason: "vip_route",
});
}
// 4. Localized agent based on caller country
const language = from.country === "IN" ? "hi-IN" : "en-US";
// 5. Pick agent + load context
return res.json({
finn_id: customer?.tier === "premium"
? "fn_premium_aria"
: "fn_standard_aria",
language,
variables: {
customer_name: customer?.name ?? "there",
account_tier: customer?.tier ?? "standard",
last_order_id: customer?.last_order_id ?? "",
},
knowledge_base_ids:
customer?.tier === "premium" ? ["kb_general", "kb_premium"] : ["kb_general"],
metadata: { ab_cohort: customer?.ab_cohort ?? "A" },
});
});
app.listen(3000);
Signature verification
Same scheme as post-call webhooks — HMAC-SHA256 over the raw body, header X-Finn-Signature.
const sig = req.headers["x-finn-signature"] as string;
const ok = verifyFinnSignature(rawBody, sig, process.env.FINN_INBOUND_SECRET!);
if (!ok) return res.status(401).send("bad signature");
See Post-call Webhooks for the full verification implementation.
Performance requirements
This is on the hot path — the caller is hearing ring tone while waiting on your response.
| Metric | Target |
|---|---|
| Response time | < 500ms (p99) |
| Timeout | 1000ms |
| Fallback on timeout | Default agent configured on the phone number |
| Fallback on 5xx | Default agent |
| Fallback on invalid JSON | Default agent + error logged |
Tips for hitting < 500ms
- Cache CRM lookups by phone number for 5 minutes
- Use a colocated database — webhook from us-east, DB in us-west = 80ms each way
- Pre-compute routing decisions — store the answer on the customer record, don't decide live
- Skip non-essential enrichment on the inbound hop — push that work to the post-call webhook
Caller pre-screening
Most apps use the inbound webhook for one of these patterns:
| Pattern | Use case |
|---|---|
| Lookup + personalize | Pull customer record, pass name + tier as variables |
| DNC enforcement | Reject calls from blocked numbers |
| VIP routing | Skip agent, transfer straight to human desk |
| Language detection | Pick hi-IN for India, en-US for US callers |
| A/B testing | Route 50% of calls to a new agent version |
| After-hours routing | Different agent (or voicemail) outside business hours |
| Campaign tracking | Tag calls from specific tracking numbers with a campaign ID |
| Multi-tenant SaaS | Route to the tenant whose number was dialed |
Testing
Use the Finn CLI to simulate inbound calls without a real PSTN ring:
finn inbound simulate \
--phone-number-id ph_1234 \
--from +919876543210 \
--webhook-url https://your-app.com/finn/inbound
Output shows your endpoint response, latency, and the resolved agent + variables. CI-friendly.
Common gotchas
| Symptom | Fix |
|---|---|
| Calls always fall back to default agent | Your endpoint is > 500ms. Check Finn's webhook log for the timing. |
| Caller hears 2-3s of silence before agent speaks | Your endpoint is slow but under timeout. Optimize to < 200ms. |
| Variables not appearing in agent speech | Mismatch between prompt placeholder {customer_name} and webhook key customerName. Use snake_case both sides. |
| Wrong agent answered | Your endpoint returned a finn_id that belongs to another org. Verify ownership. |
action: reject not honored | Check reason is a string, response JSON is valid. Invalid responses → fallback. |
Related
Was this page helpful?
Still stuck or have feedback?
Email support@hirefinn.ai or use the chat bubble in the bottom-right corner — it's a Finn that knows the Academy cold.