Skip to main content
In this tutorial you write a small phone menu in Node.js and put it on a real call. It greets the caller, asks what they need, listens for speech or a keypress, and either transfers the call to a person or says goodbye. Your code runs on your machine; Fonoster handles the phone line and the speech. A stream is the kind of application that runs your own code. For every call, Fonoster opens a WebSocket to your server and keeps it open until the call ends. The Voice SDK (@fonoster/voice) accepts that connection and gives you one session object per call.

Before you begin

You need:
  • A Fonoster account. Sign up at app.fonoster.ai, or use the dashboard of your own Fonoster install.
  • A phone number in your workspace. Link a Twilio number if you don’t have one yet.
  • Node.js 22 or later.
  • A tunnel that gives your machine a public HTTPS address. This tutorial uses ngrok.
  • A phone to call from.

Build it

1

Create the project

Make a folder for the server and install the Voice SDK:
2

Open a tunnel

Fonoster dials your server from the internet, so it needs a public address. It won’t dial localhost or a private IP unless the operator of a self-hosted install allows it.Start a tunnel to port 3000, where your server will listen:
ngrok prints a public HTTPS address:
Your stream’s URL is that address with wss:// in place of https://: wss://f3a9-198-51-100-24.ngrok-free.app. Copy it.
Keep ngrok running for the rest of this tutorial. On the free plan the address changes each time you restart it, and then you must update the stream’s URL.
3

Create the stream

Tell Fonoster where your server is. When you create a stream, Fonoster gives you its signing secret. Your server uses it to check that each connection really comes from Fonoster.
  1. Sign in to the dashboard and go to Streams.
  2. Choose New stream.
  3. Name it Front desk stream.
  4. Paste your wss:// address into URL.
  5. Keep the protocol Fonoster.
  6. Choose Create stream.
The dashboard opens your new stream and shows Signing secret (shown once). Copy it now. It isn’t shown again.
The stream uses the platform’s default voice (deepgram/aura-2-thalia-en) and language (en-US). You can change both later.
You have a stream named Front desk stream and its signing secret.
4

Write the server

Save this as server.js. VoiceServer listens on port 3000 and runs handler once for each call. The handler greets the caller, waits for an answer, and then transfers the call or says goodbye:
server.js
Each verb sends one command over the call’s WebSocket and waits for its result:
  • say(text) speaks with the stream’s voice and resolves when the caller has heard it.
  • gather() resolves with { speech } when the caller finishes talking, with { digits } when they press keys, or with neither when the timeout passes. maxDigits: 1 ends it on the first key.
  • transfer(number) hands the call to the operator. The caller stays on the line and your session ends.
  • hangup() ends the call.
Change OPERATOR to a phone you can answer if you want to try the transfer.
5

Run it

Start the server with the signing secret and the same wss:// address you gave the stream:
VoiceServer refuses to start without a signing secret, and it rejects any connection that Fonoster didn’t sign for PUBLIC_URL.
6

Point your number at the stream

Go to Numbers and open your number. Under When this number is called, send it to, choose Stream, then Front desk stream.
7

Call it

Call your number from your phone. You hear “Thanks for calling. Say sales or support, or press 0 to talk to a person.” Say “support”.Your terminal shows the call and what you said:
The stream answers “You said support. We’ll call you back soon. Goodbye.” and hangs up.Call again and press 0. You hear “One moment. I’m connecting you.”, and the call rings the operator’s number.
Your own code answered a phone call.
Fonoster tries your URL for about 2.5 seconds while the caller hears ringing. If it can’t connect, the call goes to the stream’s fallback, or ends if there is none.
  • Check that ngrok and node server.js are both running, and that the stream’s URL is the current ngrok address with wss://.
  • If the server logs rejected a session connection, the signature didn’t match. Check that APP_SIGNING_SECRET is the stream’s secret and that PUBLIC_URL is exactly the URL on the stream. See Verify request signatures.

What just happened

A caller at +1 809 555 0123 dials your number. Fonoster answers, does speech-to-text and text-to-speech, and opens one WebSocket per call to your server (server.js, VoiceServer), signed with x-fonoster-signature. Your code sends commands (say, gather, transfer, hangup) and gets events back (call started, speech heard, key pressed, speech finished, call ended), working with text, not audio. A transfer goes from Fonoster to an operator at +1 809 555 0100. A caller at +1 809 555 0123 dials your number. Fonoster answers, does speech-to-text and text-to-speech, and opens one WebSocket per call to your server (server.js, VoiceServer), signed with x-fonoster-signature. Your code sends commands (say, gather, transfer, hangup) and gets events back (call started, speech heard, key pressed, speech finished, call ended), working with text, not audio. A transfer goes from Fonoster to an operator at +1 809 555 0100. When you called, Fonoster answered the number and opened a WebSocket to your stream’s URL, signed with the stream’s secret. VoiceServer checked the signature and called your handler with a session for that call. From then on, the session carried messages both ways. Your code sent commands: speak this text, transfer to that number, hang up. Fonoster sent back events: the speech finished playing, the caller said “support”, the caller pressed 0. Fonoster did the speech-to-text and text-to-speech, so your code worked with text, not audio. When the call ended, Fonoster closed the WebSocket. To see the messages on the wire, or to write a server without the SDK, read the session protocol.

Next steps

Verify request signatures

How the signature works, and how to rotate the secret.

Use the Python Voice SDK

The same session in Python, standalone or inside FastAPI.

Transfer calls

Cold, warm and SIP REFER transfers.

Voice SDK reference

Every method, option and event, including barge-in and streaming text.