Sign in

Realtime

View as Markdown
WSSwss://voicebot-serverless-stg.botnoigroup.com/v1/call

Mint a call token with your access token, open the WebSocket with it, stream microphone audio in and receive transcripts and TTS back.

Connect

  1. 1. Mint a call token on your server

    Authenticate with an access token. Copy it from the Developers page of the Botnoi Voice console and send it on every request as Authorization: Bearer <access_token>. Post the agent you want to talk to:

    curl -s -X POST https://backend-voicebot-stg.botnoigroup.com/call/tokens \
      -H "Authorization: Bearer $ACCESS_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"agent_id": "<agent_id>", "channel": "web"}'

    Your call token comes back in ephemeral_token (vb_live_…). It is single-use and short-lived (currently about two minutes): a failed connect or a second connect needs a new one.

    Your access token never reaches the browser. Mint on your server and hand the page only the call token. The endpoint is Create a call token; every status it can return is under Errors below.

  2. 2. Connect from the browser

    Open wss://voicebot-serverless-stg.botnoigroup.com/v1/call?token=<call_token>. No headers, and no other query parameters.

    The socket opens before the token is checked, so onopen is not success — wait for opened.

NameInRequiredTypeDescription
tokenqueryYesstringYour call token from Create a call token. Single-use.

The open you send

{
  "version": "2",
  "type": "open",
  "seq": 1,
  "id": "<uuid — your session id>",
  "position": "POST-ANSWER",
  "parameters": {
    "conversationId": "<your id, [A-Za-z0-9_-]{1,128}>",
    "media": [
      {
        "type": "audio/L16",
        "sampleRateHz": 16000,
        "channels": [
          "external"
        ]
      }
    ]
  }
}

Send open as your first frame (seq 1). The agent is chosen by your token — any agent_id in inputVariables is ignored.

Every frame you send carries seq, starting at 1 and going up by one; frames out of order end the call. Server frames echo the seq they answer as clientseq.

opened echoes the media entry the server selected, with startPaused: false. Send audio only after it arrives.

Rules

  1. After opened, send microphone frames continuously — about 20 ms each, no WAV header.
  2. On every TTS burst, send playback_started when it starts playing and playback_completed when it finishes. These are required: when the agent is not interruptible, the server does not listen to the microphone until it receives playback_completed.
  3. On barge_in, stop playing TTS and clear the playback queue.
  4. To hang up, send close with reason: "end"; the server replies closed. Frames can still arrive after closed until the socket closes — ignore them; they are not an error.
  5. A call has a server-side maximum length, currently about ten minutes. At the limit the server sends disconnect, then closes the socket.

Migrating from /v1/preview_call

  • Authenticate with a token query parameter — your call token — instead of api_key and agent_id.
  • You send open yourself; the proxy used to send it for you.
  • Close codes 1008 and 1011 no longer occur; expect 4001, 3000 and 3003.
  • No headers of any kind.
  • inputVariables.agent_id is ignored — the token picks the agent.

This box goes away when /v1/preview_call is retired.

Binary audio

After opened, both directions use the selected encoding with no WAV header. Send about 20 ms per binary frame: sample_rate × 0.02 × bytes_per_sample × channels. 16 kHz L16 mono is 640 bytes; 8 kHz µ-law mono is 160 bytes.

Client messages

JSON you send, starting with open, plus raw binary audio once opened arrives.

type: openYour first frame, seq 1. The agent comes from your call token; wait for opened before sending audio.
NameTypeRequiredDescription
type"open"YesMessage type.
seq1YesYour first sequence number.
idstringYesA session id you choose; use a UUID.
positionstringYes"POST-ANSWER".
parameters.conversationIdstringYes<your id, [A-Za-z0-9_-]{1,128}>
parameters.mediaMedia[]Yes[{"type":"audio/L16","sampleRateHz":16000,"channels":["external"]}]
type: pingApplication keepalive. Not a WebSocket ping.
NameTypeRequiredDescription
type"ping"YesMessage type.
seqintegerYesPrevious client seq + 1.
type: playback_startedClient began playing a TTS burst.
NameTypeRequiredDescription
type"playback_started"YesMessage type.
type: playback_completedClient finished a TTS burst. When in-flight audio drains, recognition unmutes.
NameTypeRequiredDescription
type"playback_completed"YesMessage type.
type: closeEnd the session. Do not send more audio after this.
NameTypeRequiredDescription
type"close"YesMessage type.
parameters.reason"end" | "disconnect"YesHow the client is ending the call.
type: binary audioRaw PCM16 or µ-law frames. No WAV header. After opened only.
NameTypeRequiredDescription
(binary frame)bytesYes20 ms recommended. Encoding must match opened.parameters.media[0].

Server messages

JSON text frames the server sends, plus binary TTS. Every JSON envelope also carries version, seq, clientseq, and id.

type: openedReply to your open. Safe to send audio; echoes the selected media.
NameTypeRequiredDescription
type"opened"YesMessage type.
parameters.startPausedbooleanYesfalse
parameters.mediaMedia[]YesSingle selected descriptor. Match this on the wire.
type: pongReply to ping.
NameTypeRequiredDescription
type"pong"YesMessage type.
type: eventTranscripts and barge-in.
NameTypeRequiredDescription
type"event"YesMessage type.
parameters.entities[].typestringYesuser_turn_response | bot_turn_response | barge_in
parameters.entities[].dataobjectYesuser: { transcript.result.text, is_final, timestamp? }. bot jsonConfig: { output, action, response_type, phone_transfer }. close-end may send { disposition: match }. barge_in: {} — stop TTS and clear the playback queue; keep sending mic.
type: closedReply to close. Socket will end.
NameTypeRequiredDescription
type"closed"YesMessage type.
type: disconnectServer is ending the session.
NameTypeRequiredDescription
type"disconnect"YesMessage type.
parameters.reason"completed" | "error" | "unauthorized"YesWhy the server closed.
parameters.infostringYesHuman-readable detail, e.g. sequence mismatch or bot_id not in allow_list.
type: binary audioBot TTS in the negotiated encoding. May arrive as several frames.
NameTypeRequiredDescription
(binary frame)bytesYesPlay it, then send playback_started / playback_completed.

Errors

Minting a call token

StatusdetailWhat it means
401Not authenticated / Invalid token / Token has expired / User not foundYour access token is missing or invalid. Sign in to the console and copy it again.
402a reason, or credit_refusedNot enough credit to start a call. detail says why.
403agent_id not allowedThe agent isn't yours, or isn't linked to your account yet.
409connector_not_provisionedYour account isn't ready for calls yet. Try again in a few minutes.
422field errorsagent_id is missing or blank, or channel isn't web.
500connector_errorServer-side error. Retry; contact support if it persists.
502connector_store_unavailable / token_mint_upstream_errorAn upstream service failed. Retry.
503token_mint_not_configuredPreview calls aren't enabled on this environment.

During the call

Close codeWhat it means
4001Token invalid, expired, already used, or no capacity — mint a new token. The server does not say which.
3000Protocol error, sent after a disconnect frame: a seq out of order, or an agent that isn't allowed.
3003Billing refused the call.
1000Normal end: after closed, at the maximum call length, or when the agent ended the call.

1008 and 1011 do not occur on this WebSocket.

Message sequence

One call in wire order, as a browser client sees it. publish is what you send; subscribe is what the server sends back. Audio rows stand in for raw binary frames — they are not JSON on the wire.

publish
subscribe
{
"version": "2",
"type": "opened",
"seq": 1,
"clientseq": 1,
"id": "00000000-0000-4000-8000-000000000001",
"parameters": {
"startPaused": false,
"media": [
{
"type": "audio/L16",
"sampleRateHz": 16000,
"channels": [
"external"
]
}
]
}
}

Greeting TTS: raw PCM16 little-endian, 16 kHz, no WAV header. It starts before the greeting text.

subscribe
subscribe
{
"version": "2",
"type": "event",
"seq": 2,
"clientseq": 1,
"id": "00000000-0000-4000-8000-000000000001",
"parameters": {
"entities": [
{
"type": "bot_turn_response",
"data": {
"output": "สวัสดีค่ะ ยินดีต้อนรับค่ะ",
"action": "None",
"response_type": "text",
"phone_transfer": null,
"turn": {
"id": "t1",
"rev": 0
}
}
}
]
}
}

The next greeting chunk's audio, again before its text.

subscribe
subscribe
publish
publish

Microphone audio: 20 ms frames of PCM16, 640 bytes each at 16 kHz mono, sent continuously.

publish
subscribe
subscribe
{
"version": "2",
"type": "event",
"seq": 5,
"clientseq": 3,
"id": "00000000-0000-4000-8000-000000000001",
"parameters": {
"entities": [
{
"type": "user_turn_response",
"data": {
"transcript": {
"result": {
"text": "hello world"
}
},
"is_final": true,
"timestamp": 1767225600.98
}
}
]
}
}

Reply TTS: raw PCM16, 16 kHz, before the reply text.

subscribe
subscribe
publish
publish
publish
subscribe
subscribe

Frames may still arrive for a few seconds after closed — ignore them; they are not an error.

The socket closes with code 1000.