Skip to main content

Server integration

Local development

Tunnelling, replaying events and test numbers.

What this page covers

Local development means running your webhook handlers, tool endpoints, and inbound routing logic on your own machine while real Finn calls hit them. Three things make that work: a public tunnel, replayed events, and a test number you can dial without spending outbound credit.

Expose your local server

Finn calls your endpoint over HTTPS from the public internet. localhost:3000 is not reachable. Run a tunnel and give Finn the tunnel URL.

# ngrok
ngrok http 3000
# → Forwarding  https://a1b2c3d4.ngrok-free.app -> http://localhost:3000

# cloudflared
cloudflared tunnel --url http://localhost:3000

Then point the relevant setting at the tunnel URL. Inbound routing URLs are set per phone number:

curl https://api.hirefinn.ai/v1/phone-numbers/ph_1234 \
  -X PATCH \
  -H "Authorization: Bearer $FINN_API_KEY" \
  -d '{ "inbound_webhook_url": "https://a1b2c3d4.ngrok-free.app/finn/inbound" }'

You can also set it in the dashboard at Settings → Phone Numbers → [number] → Inbound Webhook URL. Finn pings the URL once with {"ping": true} to check reachability, so a save that appears to succeed does not mean your handler parsed the body correctly. Watch your own logs.

Post-call webhook endpoints are configured separately. See webhooks.

What goes wrong with tunnels

SymptomCause
Calls fall back to the default agentTunnel hop plus your handler exceeds the 500ms inbound budget. Timeout is 1000ms.
Caller hears 2-3s of silence before the agent speaksHandler is under the timeout but slow. See webhook-events for the latency budget.
Everything worked, then stoppedFree tunnel URLs rotate on restart. The old URL is still saved on the phone number.
Signature verification fails locallyYou are verifying the parsed body, not the raw bytes. See webhook-security.

The inbound handshake is on the hot path. The caller is listening to ring tone while your laptop, your tunnel, and your database all take a turn. A handler that is fine in production may miss the budget through a tunnel. Test correctness locally, test latency in a deployed environment.

Replay and simulate events

You do not need a real PSTN ring to exercise inbound routing. The Finn CLI simulates the call:

finn inbound simulate \
  --phone-number-id ph_1234 \
  --from +919876543210 \
  --webhook-url https://a1b2c3d4.ngrok-free.app/finn/inbound

Output shows your endpoint's response, the round-trip latency, and the agent and variables Finn resolved from it. It exits non-zero on an invalid response, so it works in CI as a contract test against your handler.

Point --webhook-url straight at http://localhost:3000 when you want to skip the tunnel and test handler logic alone. That measures your code without the tunnel hop, which is the number worth optimizing.

You can also drive the same handler with cURL, which is useful when you want a payload the CLI does not generate:

curl -X POST http://localhost:3000/finn/inbound \
  -H "Content-Type: application/json" \
  -d '{
    "session_id": "ws_local001",
    "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": {}
  }'

Signed requests will not verify against a hand-written cURL body unless you compute the HMAC yourself. Gate verification behind an environment flag in development rather than deleting the check, so it cannot ship disabled.

Test calls

Trigger a test call from the dashboard rather than the API. New accounts start with $25 of credit, visible at Settings → Plan and billing → Usage.

Test calls fail for reasons that look like bugs but are not:

SymptomCheck
No call arrivesPhone number needs a country code. +15551234567, +919876543210.
No call arrivesTrial credit exhausted. Settings → Plan and billing → Usage.
No call arrivesYour number is on the global suppression list. Settings → Compliance → Do Not Call List.
No call arrivesCarrier or handset spam filter dropped it silently. Try a different from-number.
Call connects, agent is silentWelcome message set to defined with an empty field. Set text or switch to dynamic.
Call connects, agent is silentVoice not assigned, or assigned voice needs a region match. Re-select it.
Wrong agent answeredYour handler returned a finn_id belonging to another org.

Variables that do not reach the agent are almost always a naming mismatch. The prompt placeholder {customer_name} must match the webhook key exactly. Use snake_case on both sides. See agents-variables.

A workable loop

  1. Start your server and tunnel.
  2. Point the phone number at the tunnel URL.
  3. Iterate with finn inbound simulate against localhost until the response shape is right.
  4. Run the simulator through the tunnel once to confirm reachability and check reported latency.
  5. Place one real test call from the dashboard.
  6. Read the transcript and resolved variables in call-logs.

Steps 3 and 5 catch different failures. The simulator never proves audio works, and a test call rarely tells you which line of your handler was wrong.