Skip to main content

Server integration

Local development

Tunnelling, replaying events and test calls.

What this page covers

Local development means running your webhook handler on your own machine while real Finn calls produce events for it. Three things make that work: a public tunnel, replayed events, and a test call placed from the dashboard.

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 register the tunnel URL as a webhook endpoint in the dashboard at Settings → Integrations → Webhooks (for example https://a1b2c3d4.ngrok-free.app/finn/webhooks). Finn delivers call.completed events there, signed as described in webhook-security. Endpoints must be https, which the tunnel provides. Inbound call routing webhooks are not available yet.

What goes wrong with tunnels

SymptomCause
Everything worked, then stoppedFree tunnel URLs rotate on restart. Add the new URL as a webhook endpoint and delete the old one.
Deliveries arrive but time outYour handler takes longer than 10 seconds through the tunnel. Respond first, process after.
Signature verification fails locallyYou are verifying the parsed body, not the raw bytes. See webhook-security.

Finn waits 10 seconds for your endpoint to answer each delivery attempt. A tunnel adds latency, so return 2xx quickly and do slow work after responding. See webhook-retries.

Replay and simulate events

You do not need a real call to exercise your handler. Take the example body from webhook-post-call, save it as event.json, and sign it the way Finn does:

SECRET='whsec_your_endpoint_secret'
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$(cat event.json)" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')

curl -X POST http://localhost:3000/finn/webhooks \
  -H "Content-Type: application/json" \
  -H "X-Finn-Signature: t=$T,v1=$SIG" \
  --data-binary @event.json

--data-binary sends the file byte for byte, so the signature computed over the file matches what your handler receives. If verification still fails, your handler is probably verifying the parsed body instead of the raw bytes. See webhook-security.

Test calls

Trigger a test call from the dashboard rather than the API. New accounts start with free trial 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 arrivesCarrier or handset spam filter dropped it silently. Try a different from-number.
Call connects, agent is silentVoice not assigned, or assigned voice needs a region match. Re-select it.

Variables only reach a call from a deployment's audience, so a test call never fills them. On deployed calls, a variable that isn't filled is usually a syntax or naming problem: use double braces, {{ customer_name }}, only in the welcome message or system prompt, and make the name match a column header (case, spaces and punctuation are ignored). See agents-variables.

A workable loop

  1. Start your server and tunnel.
  2. Register the tunnel URL in Settings → Integrations → Webhooks and save the signing secret.
  3. Replay a signed call.completed body against localhost until your handler verifies and stores it correctly.
  4. Place one real test call from the dashboard and confirm the real delivery arrives through the tunnel.
  5. Compare what you stored with call-logs or GET /api/v1/calls/{call_uuid}.

Steps 3 and 4 catch different failures. A replay never proves delivery works end to end, and a real call rarely tells you which line of your handler was wrong.