Skip to main content

Server integration

Caller pre-screening

Deciding how to greet someone before they speak.

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.

OutcomeResponse shapeWhen to use
Answer with an agent{ "finn_id": "fn_..." } plus optional overridesThe 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 usabletimeout, 5xx, or invalid JSONNot 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

PatternWhat the endpoint does
Lookup and personalizePull the customer record, pass name and tier as variables
DNC enforcementReject calls from numbers on your blocked list
VIP routingSkip the agent, transfer straight to a human desk
Language selectionReturn hi-IN for India callers, en-US for US callers
A/B testingRoute a share of calls to a new agent version
After-hours routingDifferent agent, or voicemail, outside business hours
Campaign trackingTag calls from tracking numbers with a campaign ID in metadata
Multi-tenant SaaSRoute 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.

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

SymptomCause
Every call gets the default agentEndpoint exceeds 500ms, or returns invalid JSON. Check the webhook log timings.
Two to three seconds of silence before the agent speaksEndpoint is slow but under the 1000ms timeout.
Agent says the placeholder name out loudVariable key and prompt placeholder differ. Use snake_case on both.
Wrong agent answeredThe finn_id you returned belongs to another org.
action: reject ignoredResponse JSON was invalid, so Finn fell back. reason must be a string.