Skip to main content
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:// or wss://. 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 (fallbackAppRef or fallbackMessage), or ends the call.
  • Answering: inbound calls are answered when session.start is 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 an id, a string of 1 to 128 characters. Events a command causes echo it (output.* events as id, errors as relatesTo).
  • 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 optional id. 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.
Fonoster starts speech recognition on the first 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 send transfer.complete to connect them and end your session. If nobody answers in 30 seconds you get error transfer_failed and the caller is back with you.
  • refer: a SIP REFER; your session ends with transfer.
An unreachable target gets 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

  • pcm16 is signed 16-bit little-endian, mono. pcmu is G.711 μ-law, mono, 8000 Hz.
  • The stream’s audio setting picks the format. The default is pcm16 at 16000 Hz.
  • Caller audio arrives on track 1 (side inbound).

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 4 listeners (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.

Limits