Conversation Relay

Conversation Relay: Text-In, Text-Out Voice Bots in India

Edesy Conversation Relay connects an inbound call on your Indian number to your bot over a WebSocket using Twilio's ConversationRelay protocol. Your bot receives transcripts and sends text back; Edesy runs speech recognition, speech synthesis, barge-in and DTMF. A bot written for Twilio works unchanged.

Text only

No audio code in your bot

Compatible

Twilio ConversationRelay protocol

Barge-in

Tunable per number

Preflight

Test without a call

ConversationRelay-compatible

Speech handled by Edesy

Indian numbers

INR wallet + GST invoices

Conversation Relay at a glance

Protocol Twilio ConversationRelay wire protocol: JSON text frames over one WebSocket per call
Your bot handles Text only: transcripts in, reply tokens out
Edesy handles Speech recognition, speech synthesis, barge-in and DTMF detection
Calls Inbound calls to your Edesy number
Setup Dashboard (When a call comes in > AI voice bot, text in, text out) or PATCH /v1/phone-numbers/{id}/routing
Settings Welcome greeting, voice, language, interruptible, min_bargein_word_count, DTMF detection, custom parameters
Mid-call Switch language with a language message; play audio; send DTMF; end with handoffData
Preflight POST /v1/relay-test: DNS, TCP, TLS, upgrade and protocol checks
Call events call.relay.connected, call.relay.failed, call.relay.handoff, call.relay.usage
Billing Prepaid INR wallet. Relay includes speech processing, so it is priced separately from plain call minutes: ask us for the rate

Put your LLM on a phone number

No audio pipeline, SIP stack or telephony code on your side

  1. Step 1 of 4: Run a WebSocket bot

    Accept the connection, read setup, answer each prompt by streaming text tokens, and set last: true on the final one. The Node sample is about 90 lines.

  2. Step 2 of 4: Test it from Edesy's network

    POST /v1/relay-test with your relay_url. You get a dns, tcp, tls, upgrade and protocol checklist, and a hint if your bot never sends last: true.

  3. Step 3 of 4: Point a number at it

    In the dashboard choose AI voice bot (text in, text out) under When a call comes in, or PATCH the number's routing with inbound_target_type relay.

  4. Step 4 of 4: Call it and read the events

    GET /v1/calls/{sid}/events shows call.relay.connected or call.relay.failed with the reason, the bot's handoff, and usage per call.

The protocol

Every frame is JSON with a type.

Edesy to your bot

Type When Fields
setup Once, before anything is spoken sessionId, callSid, from, to, direction, callType, customParameters
prompt The caller finished an utterance voicePrompt, lang, last
interrupt The caller talked over the bot utteranceUntilInterrupt, durationUntilInterruptMs
dtmf One keypress (when DTMF detection is on) digit
error A platform-side problem, informational description

Your bot to Edesy

Type Effect Fields
text Speak it. Stream tokens as your LLM produces them token, last, interruptible, preemptible
play Play an audio file, then resume the conversation source, loop
sendDigits Send DTMF out of the call (w waits half a second) digits
language Switch speech and transcription language mid-call ttsLanguage, transcriptionLanguage
end End the call. handoffData is recorded as a call.relay.handoff event handoffData

Three things that catch people out

Set last: true on the final token of a turn. It flushes the speech synthesizer. Without it the end of every reply is never spoken. The relay test reports this for you.

Handle interrupt. When the caller barges in, unplayed speech is discarded, so your history holds words the caller never heard. Trim the last turn to utteranceUntilInterrupt.

Accept the WebSocket upgrade quickly. The connection attempt is bounded (3 seconds by default) and a fallback line is spoken if it expires. Authenticate at the upgrade, not after it.

Number settings

Sent as inbound_relay_config. Sending it replaces the stored object; it is not merged.

Field Default Notes
welcome_greeting none Spoken on connect, before the caller speaks. Omit it to let your bot speak first.
tts_voice platform default The voice your bot speaks with.
tts_language / stt_language en-IN Speech and transcription language.
dtmf_detection false Keypresses interrupt the bot and arrive as dtmf.
interruptible true Whether the caller can talk over the bot.
min_bargein_word_count 1 Words of caller speech needed to interrupt. 0 interrupts on any voice activity.
custom_parameters none Passed through unchanged in setup.

Configure a number and run the preflight

The routing update stores the relay URL and settings on the number and re-points its call handling at the relay. The relay test exchanges one turn with your bot before any caller does.

# Point a number at your relay bot
curl -X PATCH https://voice-api.edesy.in/v1/phone-numbers/$PHONE_NUMBER_ID/routing \
  -H "Authorization: Bearer $EDESY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "inbound_target_type": "relay",
    "inbound_relay_url": "wss://bot.example.com/relay",
    "inbound_relay_config": {
      "welcome_greeting": "Hi, thanks for calling Acme. How can I help?",
      "tts_language": "en-IN",
      "stt_language": "en-IN",
      "interruptible": true,
      "min_bargein_word_count": 1,
      "dtmf_detection": true,
      "custom_parameters": {"store": "pune"}
    }
  }'

# Preflight: one turn of conversation from Edesy's network, no call
curl -X POST https://voice-api.edesy.in/v1/relay-test \
  -H "Authorization: Bearer $EDESY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"relay_url": "wss://bot.example.com/relay", "prompt": "what are your hours", "seconds": 5}'

A complete bot in Node.js

Adapted from the conversation-relay-edesy sample: it receives text and sends text. Replace replyTo() with your LLM and it is a working phone bot.

// Minimal relay bot (Node.js, ws). Swap replyTo() for your LLM call.
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ port: 8787, path: '/relay' });

wss.on('connection', (ws) => {
  const history = [];
  ws.on('message', (raw) => {
    const msg = JSON.parse(raw);
    switch (msg.type) {
      case 'setup':      // once per call: callSid, from, to, customParameters
        break;
      case 'prompt':     // the caller finished speaking
        history.push({ role: 'user', text: msg.voicePrompt });
        speak(ws, replyTo(msg.voicePrompt, history));
        break;
      case 'interrupt':  // trim the bot's last turn to what was heard
        if (history.at(-1)?.role === 'bot') history.at(-1).text = msg.utteranceUntilInterrupt;
        break;
      case 'dtmf':
        speak(ws, `You pressed ${msg.digit}.`);
        break;
    }
  });
});

function speak(ws, text) {
  const words = text.split(' ');
  words.forEach((w, i) => ws.send(JSON.stringify({
    type: 'text', token: i ? ` ${w}` : w,
    last: i === words.length - 1,   // flushes speech synthesis
  })));
}

What Edesy runs so your bot doesn't have to

The audio side of a phone conversation

Speech recognition

Each caller utterance arrives as a prompt with the transcript in voicePrompt and the language in lang.

Speech synthesis

Stream reply tokens as your LLM produces them; Edesy speaks them in the voice and language set on the number.

Barge-in

Callers can talk over the bot. Tune it with interruptible and min_bargein_word_count, and get an interrupt with what was heard.

DTMF both ways

With dtmf_detection on, keypresses arrive as dtmf messages. sendDigits presses keys out of the call, for example into another IVR.

Mid-call language switch

Send a language message to change ttsLanguage and transcriptionLanguage without dropping the call.

Clean endings and handoff

end closes the call and records your handoffData as a call.relay.handoff event your systems can read.

Relay, Media Streams or SIP?

Full comparison: choosing SIP, stream or relay

Conversation Relay: FAQ

Want the Conversation Relay rate?

Tell us about your bot and your expected monthly minutes. We'll share the relay rate and help with the first call.

Bring the LLM. We'll handle the phone call.

Point an Indian number at your relay bot and test it from our network before the first caller rings.