fonoster.session.v1 is the protocol a stream with protocol fonoster speaks. Each call is one
WebSocket that Fonoster opens to your stream’s URL. Your server receives the caller’s audio and
events, and sends commands. Use this page to write a client in a language without a
Voice SDK. The machine-readable definition is the JSON Schema
mods/common/schema/session.v1.json, one schema per message.
Connection
Fonoster dials your server; your server never dials Fonoster.- URL: the stream’s
url,ws://orwss://. Fonoster dials public addresses only, unless the operator of a self-hosted install allows others. - Retries: if the connection fails, Fonoster retries every 250 ms for 2.5 seconds while the
caller hears ringing. Then it applies the stream’s fallback (
fallbackAppReforfallbackMessage), or ends the call. - Answering: inbound calls are answered when
session.startis sent. There is no answer command. On outbound calls, the session opens when the callee answers. - Keepalive: Fonoster pings every 10 seconds. A socket that sends nothing (no pong, no frame) for 30 seconds is dropped and the call is hung up.
Handshake headers
Every connection request is signed with the stream’s signing secret:<url> is the stream’s URL exactly as configured, query string included. Reject a request whose
signature doesn’t match, or whose timestamp is more than 300 seconds from your clock. See
Verify request signatures.
Frames
- Text frames carry one JSON message each. Every message has a
type. - Binary frames carry audio. See Audio.
id: commands can carry anid, a string of 1 to 128 characters. Events a command causes echo it (output.*events asid, errors asrelatesTo).- Unknown types and fields: ignore them. The protocol only adds types and fields; a breaking change gets a new protocol name. Fonoster ignores unknown command types too.
Session lifecycle
Fonoster → your server
Always the first message.
Fonoster → your server
The last message.
reason is caller_hangup, app_hangup, transfer, handoff, error or
timeout. Fonoster then closes the socket with code 1000.Closing
Commands (your server → Fonoster)
Every command takes an optionalid. Fields marked required must be present, or Fonoster answers
error with code bad_message.
Speech
command
Speaks text with text-to-speech.
Without an
id, Fonoster assigns one. A voice not in the catalog gets error
invalid_catalog_id.command
Plays a WAV file (PCM or μ-law, any rate), converted to the call’s format.
A file that can’t be fetched gets
error play_fetch_failed.command
Starts listening, or changes recognition from now on. All fields optional:
language, hints
(array of strings, added to the stream’s hints) and patience (low, normal or high).command
events (required): opt-in events to receive, from turn.estimate and transcript.partial.
Other names are ignored.speak, play, subscribe or listen.update.
Before that it sends no turn events. Keypresses always arrive.
Output queue
command
name (required, non-empty): marks a position in the output queue. Fonoster answers with a
mark event.command
Empties the whole output queue. The caller hears silence within 40 ms. Pending items end with
output.interrupted, and pending marks are answered with cleared: true.speak, play and the audio you send share one queue, played in real time. New items play after
earlier ones. More than 120 seconds of queued output gets error queue_full.
Call control
command
Hangs up. You get
session.end with app_hangup.command
digits (required): keys to send on the call, matching ^[0-9A-D*#w]+$. w is a pause.command
Transfers the call.
- cold: the new party joins and your session ends with
transfer. - warm: the caller waits on hold while you talk to the new party. When the new party
answers you get
transfer.answered; then sendtransfer.completeto connect them and end your session. If nobody answers in 30 seconds you geterrortransfer_failedand the caller is back with you. - refer: a SIP REFER; your session ends with
transfer.
error transfer_failed. See Transfer calls.command
id (required): the id of the pending warm transfer. Connects the caller and the new party,
and ends your session with transfer. Before transfer.answered it is refused with error
transfer_not_answered (not fatal): nothing changes, the caller stays on hold, and you can send
it again once the new party answered.command
Puts the caller on hold.
music (optional): an http or https URL, looped; silence without
it. While on hold you get no caller audio and no turn events (keypresses still arrive), and
every command except resume, hangup, transfer and handoff gets error on_hold.command
Takes the caller off hold.
command
Hands the call to another application in the same workspace.
app (required): its ref.
payload (optional): a JSON object up to 16 KB. You get session.end with handoff; the
other application gets session.start with your payload in handoff. At most 5 handoffs per
call. An unknown application gets error bad_message.Events (Fonoster → your server)
Output
Turns
A turn arrives as
turn.user_started, zero or more turn.likely_ended and turn.resumed
pairs, an optional turn.likely_ended, then turn.ended. See
Turn-taking and barge-in.
Other events
Audio
pcm16is signed 16-bit little-endian, mono.pcmuis G.711 μ-law, mono, 8000 Hz.- The stream’s
audiosetting picks the format. The default ispcm16at 16000 Hz. - Caller audio arrives on track
1(sideinbound).
Errors
error { code, message, fatal, relatesTo }. In this version no error is fatal on its own; a
session that can’t continue ends with session.end.
Fonoster never switches providers on failure; it reports the error.
Listen-only sockets
A stream can list up to 4listeners (url, tracks: inbound, outbound or both, default
both). Fonoster dials each one when the session starts, signed with the stream’s secret.
Listeners get session.start, the same events as your server, and the tracks they chose as
separate binary frames, each with its track id. Sides are never mixed. Anything a listener sends
gets error read_only, and its audio is dropped.