What pre-screening means
Pre-screening is the decision your server makes between the moment a call arrives and the moment the agent speaks. Finn answers the ring, POSTs the caller's details to your inbound webhook, and waits for you to say which agent answers, in what language, with what context loaded. The caller hears ring tone the whole time.
This page covers the decisions. For the transport, payload shape, and signature verification, see webhook-inbound.
The four possible outcomes
Your response resolves to one of four things.
| Outcome | Response shape | When to use |
|---|---|---|
| Answer with an agent | { "finn_id": "fn_..." } plus optional overrides | The default. Most calls. |
| Reject | { "action": "reject", "reason": "..." } | DNC list, blocked account, after-hours with no voicemail path |
| Transfer to a human | { "action": "transfer", "to": "+91...", "reason": "..." } | VIP tier, escalation, known-broken account state |
| Nothing usable | timeout, 5xx, or invalid JSON | Not a choice. Finn falls back to the number's default agent. |
The fourth row is the one to design around. A pre-screening endpoint that is slow or flaky does not fail loudly. It quietly answers every call with the default agent, personalizes nothing, and enforces none of your rules. See Failure is silent below.
Common patterns
| Pattern | What the endpoint does |
|---|---|
| Lookup and personalize | Pull the customer record, pass name and tier as variables |
| DNC enforcement | Reject calls from numbers on your blocked list |
| VIP routing | Skip the agent, transfer straight to a human desk |
| Language selection | Return hi-IN for India callers, en-US for US callers |
| A/B testing | Route a share of calls to a new agent version |
| After-hours routing | Different agent, or voicemail, outside business hours |
| Campaign tracking | Tag calls from tracking numbers with a campaign ID in metadata |
| Multi-tenant SaaS | Route to the tenant who owns the dialed number |
Screening on identity
The from block is what you have before anyone speaks. For PSTN it carries phone, country, and carrier. For WhatsApp it carries wa_id, display_name, and profile_pic_url, and channel is whatsapp instead of pstn. Handle both if the number takes both.
import express from "express";
const app = express();
app.use(express.json());
const OPEN_HOUR = 9;
const CLOSE_HOUR = 18;
app.post("/finn/inbound", async (req, res) => {
const { from, received_at } = req.body;
// PSTN gives from.phone, WhatsApp gives from.wa_id
const phone = from.phone ?? `+${from.wa_id}`;
const customer = await crm.findByPhone(phone); // indexed lookup, cached
if (customer?.dnc) {
return res.json({ action: "reject", reason: "dnc" });
}
if (customer?.tier === "vip") {
return res.json({
action: "transfer",
to: process.env.VIP_DESK_NUMBER,
reason: "vip_route",
});
}
const hour = new Date(received_at).getUTCHours() + 5; // IST offset
const openNow = hour >= OPEN_HOUR && hour < CLOSE_HOUR;
return res.json({
finn_id: openNow ? "fn_daytime_desk" : "fn_after_hours",
language: from.country === "IN" ? "hi-IN" : "en-US",
variables: {
customer_name: customer?.name ?? "there",
account_tier: customer?.tier ?? "standard",
},
knowledge_base_ids: customer?.tier === "premium"
? ["kb_general", "kb_premium"]
: ["kb_general"],
metadata: { ab_cohort: customer?.ab_cohort ?? "A" },
});
});
app.listen(3000);
Variables you return land in the prompt as {customer_name}. The key you send and the placeholder in the prompt must match exactly, snake_case on both sides. A mismatch does not error. The agent reads the literal placeholder aloud. See agents-variables.
Failure is silent
Your endpoint sits on the hot path.
| Metric | Value |
|---|---|
| Response time target | under 500ms (p99) |
| Timeout | 1000ms |
| Fallback on timeout | Default agent on the phone number |
| Fallback on 5xx | Default agent |
| Fallback on invalid JSON | Default agent, error logged |
Consequences worth stating plainly. If your DNC check lives only in this endpoint and the endpoint times out, Finn answers the blocked caller with the default agent. If your VIP transfer lives only here, the VIP gets the standard agent. Fallback is not a safe default for enforcement rules, only for personalization.
Two things follow. First, configure the fallback agent deliberately, because it is what actually answers when things break. Second, keep hard rules such as DNC in a second place, not only in the hot path. Global suppression is configured in the dashboard under Settings, Compliance, Do Not Call List, and applies regardless of what your webhook returns.
Keeping it under 500ms
- Cache lookups by phone number. A five-minute TTL is usually enough, since caller records rarely change mid-call.
- Colocate the database with the endpoint. A cross-region hop costs tens of milliseconds each way and you pay it twice.
- Pre-compute the routing decision and store it on the customer record. Do not decide live.
- Do not enrich here. Anything not needed to pick an agent belongs in the post-call webhook.
A slow endpoint that stays under the timeout is its own symptom. The caller hears two to three seconds of silence before the agent speaks, and nothing in the logs looks broken.
Testing
Simulate an inbound call without a real ring:
finn inbound simulate \
--phone-number-id ph_1234 \
--from +919876543210 \
--webhook-url https://your-app.com/finn/inbound
The output shows your endpoint's response, its latency, and the resolved agent and variables. It exits with a status code, so you can run it in CI.
Diagnosing
| Symptom | Cause |
|---|---|
| Every call gets the default agent | Endpoint exceeds 500ms, or returns invalid JSON. Check the webhook log timings. |
| Two to three seconds of silence before the agent speaks | Endpoint is slow but under the 1000ms timeout. |
| Agent says the placeholder name out loud | Variable key and prompt placeholder differ. Use snake_case on both. |
| Wrong agent answered | The finn_id you returned belongs to another org. |
action: reject ignored | Response JSON was invalid, so Finn fell back. reason must be a string. |
Related
- webhook-inbound — payload, field reference, signature verification
- inbound-routing — routing without a webhook
- agents-variables — how returned variables reach the prompt
- phone-numbers — where the fallback agent is configured
- whatsapp-calling — WhatsApp
fromblock