Skip to content

Gandr TTS in Twilio calls.

Two patterns: render an mp3 on your server and return a TwiML Play URL, or stream PCM into a Media Streams websocket after transcoding to 8 kHz mulaw.

Ask Claude CodeAsk ChatGPT

Render-then-serve (Node)

Your server calls the Gandr API, saves the mp3, serves it at a public URL, then returns TwiML that plays it. Twilio fetches the file and plays it to the caller.

Recipe
// server.js (Node, Express)
  // npm install express node-fetch
  import express from 'express'
  import { createWriteStream } from 'fs'
  import { pipeline } from 'stream/promises'
  
  const app = express()
  app.use(express.urlencoded({ extended: false }))
  
  // Serve the cached audio file
  app.use('/audio', express.static('audio'))
  
  // Twilio calls this when the call connects
  app.post('/voice', async (req, res) => {
    const text = 'Thank you for calling. Your account balance is ready.'
  
    const ttsRes = await fetch('https://tts.gandr.ai/v1/audio/speech', {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer gnd_...',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: 'tts-1',
        input: text,
        voice: 'gandr-mia',
        response_format: 'mp3',
      }),
    })
  
    // Save to disk, then hand Twilio a stable URL
    await pipeline(ttsRes.body, createWriteStream('audio/reply.mp3'))
  
    res.type('text/xml')
    res.send(`<?xml version="1.0" encoding="UTF-8"?>
  <Response>
    <Play>https://yourserver.example.com/audio/reply.mp3</Play>
  </Response>`)
  })
  
  app.listen(3000)

The URL in the Play verb must be reachable from the public internet. If you are developing locally, use a tunnel such as a reverse proxy to your localhost. Twilio fetches the file over HTTPS.

What you need

What it takes

A Gandr API keyBearer gnd_... in the Authorization header
A web servermust be reachable by Twilio over the public internet
A Twilio accountconfigured to point to your server webhook URL
Node or any HTTP serverexamples use Node; any language works

Why render-then-serve

Twilio fetches the URL in a Play verb and streams it directly to the caller. Your server controls the audio file: you can pre-render common phrases at startup and serve them from disk without a Gandr API call at call time, or render dynamically per call and cache the result.

The mp3 format is the simplest choice for TwiML Play. No transcoding is needed. Twilio handles playback for you.

Media Streams: PCM to mulaw 8 kHz

Twilio Media Streams sends and expects mulaw encoded audio at 8000 Hz over a websocket. Gandr PCM is headerless signed 16-bit little-endian mono at 24000 Hz. The two rates and encodings do not match, so a transcode step is required.

The commands below convert a Gandr PCM stream to mulaw 8000 Hz. Run either in a pipe between the Gandr response body and your websocket write loop.

  • ffmpeg: ffmpeg -f s16le -ar 24000 -ac 1 -i pipe:0 -ar 8000 -f mulaw -ac 1 pipe:1
  • sox: sox -t raw -r 24000 -e signed-integer -b 16 -c 1 - -t raw -r 8000 -e u-law -b 8 -c 1 -

Media Streams pattern (Node websocket)

The outline below shows where the transcode sits. Fill in your websocket library and stream handling.

  • // 1. Open Gandr PCM stream const gandrRes = await fetch('https://tts.gandr.ai/v1/audio/speech', { method: 'POST', headers: { 'Authorization': 'Bearer gnd_...', 'Content-Type': 'application/json' }, body: JSON.stringify({ model: 'tts-1', input: text, voice: 'gandr-mia', response_format: 'pcm' }), }) // 2. Pipe through ffmpeg transcode to mulaw 8 kHz // ffmpeg -f s16le -ar 24000 -ac 1 -i pipe:0 -ar 8000 -f mulaw -ac 1 pipe:1 // 3. Forward mulaw chunks to the Twilio Media Streams websocket // Each chunk must be base64-encoded and sent as a Twilio media message

Voice and language options

Six voices are available: gandr-mia, gandr-ava, gandr-jenny, gandr-dane, gandr-leo, and gandr-lewis. All six voices cover 23 languages and every render is watermarked.

Latency for a warm request: first audio byte in 146 ms over the open internet, 116 ms p50 first audio, server side warm.

Input length

Each request accepts up to 2000 characters. For IVR scripts longer than that, split the text at sentence boundaries and make one request per segment.

Notes

Does Gandr return audio fast enough for live IVR use?

First audio byte in 146 ms over the open internet, 116 ms p50 first audio, server side warm. For the render-then-serve pattern your server fetches the full file before Twilio plays it, so the caller hears audio after your server finishes writing the file.

Which format is best for TwiML Play?

mp3 is the simplest. Return it in the Play verb and Twilio handles the rest. No sample rate conversion or encoding step is required.

Why does the Media Streams path require a transcode?

Twilio Media Streams uses mulaw 8000 Hz. Gandr PCM is s16le 24000 Hz. The two formats do not match, so a transcode is necessary. The ffmpeg or sox one-liner above handles this in a single pipe stage.

Can I pre-render all my IVR prompts at startup?

Yes. Render each phrase once at startup, save the mp3 files to disk, and serve them from a static directory. The Gandr API is not called during live calls. This also removes per-call latency from the render step.

Is there a per-request character limit?

Each request accepts up to 2000 characters. Split longer scripts into segments and render each separately.

A key and one server endpoint, and your Twilio calls can speak in any of 23 languages.

Get a key

Full API reference , gandr.ai/docs