Skip to main content

Inbound Webhooks

Dynamic agent routing — Finn calls your server when an inbound call lands.

6 min read

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

FieldRequired?Notes
finn_idYesWhich agent answers. Must belong to your org.
languageNoOverride agent's default language. en-US, hi-IN, es-ES, etc.
variablesNoFree-form key/value bag. Available inside the prompt as {variable_name}.
knowledge_base_idsNoOverride which KBs are loaded for this call.
metadataNoAttached to the call record and post-call webhook. Use for your own analytics tagging.
recording_enabledNoOverride the agent default.
max_call_duration_secondsNoHard 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.

MetricTarget
Response time< 500ms (p99)
Timeout1000ms
Fallback on timeoutDefault agent configured on the phone number
Fallback on 5xxDefault agent
Fallback on invalid JSONDefault 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:

PatternUse case
Lookup + personalizePull customer record, pass name + tier as variables
DNC enforcementReject calls from blocked numbers
VIP routingSkip agent, transfer straight to human desk
Language detectionPick hi-IN for India, en-US for US callers
A/B testingRoute 50% of calls to a new agent version
After-hours routingDifferent agent (or voicemail) outside business hours
Campaign trackingTag calls from specific tracking numbers with a campaign ID
Multi-tenant SaaSRoute 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

SymptomFix
Calls always fall back to default agentYour endpoint is > 500ms. Check Finn's webhook log for the timing.
Caller hears 2-3s of silence before agent speaksYour endpoint is slow but under timeout. Optimize to < 200ms.
Variables not appearing in agent speechMismatch between prompt placeholder {customer_name} and webhook key customerName. Use snake_case both sides.
Wrong agent answeredYour endpoint returned a finn_id that belongs to another org. Verify ownership.
action: reject not honoredCheck reason is a string, response JSON is valid. Invalid responses → fallback.

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.