Add a better voice to Deepgram's Voice Agent with Speechify

8 min read

Point Deepgram Voice Agent at the open-source tts-shims OpenAI-compatible proxy to speak with a Speechify voice. The shim answers Deepgram's open_ai TTS request and keeps your Speechify key server-side.

Developer Relations · SpeechifyAI Labs

You give Deepgram Voice Agent a Speechify voice by running the OpenAI-compatible binary from tts-shims and setting Deepgram’s TTS provider to open_ai with the endpoint.url pointed at that shim. Deepgram already knows how to talk to a bring-your-own OpenAI TTS endpoint, so it sends a normal POST /v1/audio/speech request, and the shim answers it with audio from Speechify while your key stays on the server. The important bit is that this uses the tts-shims openai binary, not the deepgram binary.

The finished demo is in speechify-ai-demos/deepgram-voice-agent-shim. You’ll need a synth-capable Speechify key to run the smoke test. Get one here if you do not have one yet.

Why not put the Speechify key in Deepgram’s settings?

Deepgram’s custom TTS config needs a credential it can send to whatever endpoint it calls. That Settings message is agent configuration, not your server environment, so a real Speechify key in endpoint.headers.authorization becomes part of the agent setup and travels wherever Deepgram sends it. That is the problem the shim exists to solve.

The shim makes the credential disposable. Deepgram sends a placeholder Bearer token, the shim reads SPEECHIFY_API_KEY from its own environment and calls Speechify with that, and the token Deepgram sent is discarded. It is the same server-side-key pattern as the voice cloning web app, packaged as a small Go proxy rather than a Next.js route handler.

How does Deepgram Voice Agent call a third-party TTS?

Deepgram Voice Agent supports bring-your-own TTS through the agent.speak block of its Settings message. For a third party you set provider.type to the provider dialect and give an endpoint with a url and headers. The supported third-party types are open_ai, eleven_labs, cartesia, and aws_polly. There is no “custom Deepgram-dialect endpoint” option: provider.type: "deepgram" means Deepgram’s own hosted TTS, and it takes no endpoint override at all.

The one that fits Speechify is open_ai. When you use it, Deepgram sends a request in OpenAI’s text-to-speech shape to your endpoint.url:

{ "model": "tts-1", "input": "The text the agent wants to say.", "voice": "alloy" }

That is exactly the request the openai shim answers. So the integration has three moving parts: run the OpenAI-compatible shim, point Deepgram’s open_ai endpoint at it, and let the shim translate the speak request to Speechify.

Deepgram Voice Agent

The shim holds the Speechify key

Deepgram sends normal speak requests to your shim. The shim keeps the real API key in its server environment and streams audio back from Speechify.

  1. Deepgram agent Calls custom TTS With provider.type: "open_ai" it sends an OpenAI-shaped request and a placeholder Bearer token. POST /v1/audio/speech
  2. tts-shims Holds the key Reads SPEECHIFY_API_KEY from its own environment. ./bin/openai
  3. Speechify API Streams audio Synthesizes the text and returns audio bytes through the shim. POST /v1/audio/stream

The settings message configures Deepgram. The shim only receives the resulting OpenAI-shaped {"input":"..."} speak request, ignores the placeholder token, and calls Speechify with the server-side key.

Build and run the shim

Clone the shim repo and build the OpenAI binary. Each provider is a separate binary under cmd/, so you build only the one you need:

git clone https://github.com/Speechify-AI/tts-shims
cd tts-shims
make openai

The build is plain Go with no dependencies. In the verified run for this post, go version reported go1.26.3 darwin/arm64 and make openai produced ./bin/openai. Start it on a local port:

export SPEECHIFY_API_KEY=sk_your_key_here
export SHIM_ADDR=:8771

./bin/openai

The binary reads a small set of environment variables shared across all the shim providers: SPEECHIFY_API_KEY, SHIM_ADDR, SPEECHIFY_BASE_URL, SPEECHIFY_VERSION, SHIM_DEFAULT_MODEL, SHIM_REQUEST_TIMEOUT, and SHIM_SHUTDOWN_TIMEOUT. Only SPEECHIFY_API_KEY is required.

Confirm it’s up:

curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8771/healthz

That returned 200 on my run.

Verify audio flows through the shim

The openai shim listens on POST /v1/audio/speech, accepts Authorization: Bearer, and reads model, input, voice, and response_format from the JSON body. That is the request Deepgram will send, so you can prove the whole path with one curl before you touch Deepgram at all:

curl -s -o shim-smoke.mp3 -w "%{http_code} %{size_download}\n" \
  -X POST "http://localhost:8771/v1/audio/speech" \
  -H "Authorization: Bearer placeholder-ignored-by-shim" \
  -H "Content-Type: application/json" \
  -d '{"model":"simba-3.2","input":"Deepgram voice agent, now speaking with Speechify.","voice":"geffen_32","response_format":"mp3"}'

The verified result was real audio from Speechify:

200 60333 bytes
shim-smoke.mp3: MPEG ADTS, layer III, v2, 128 kbps, 24 kHz, Monaural

At this point the shim path is proven before Deepgram enters the picture. It built, started, passed /healthz, took an OpenAI-shaped request with a throwaway Bearer token, called the live Speechify API, and returned a playable 24 kHz MP3.

Point Deepgram’s settings at the shim

In the Deepgram Voice Agent Settings message, set the TTS provider to open_ai and give it an endpoint pointing at your deployed shim. The full settings shape is Deepgram’s, so use Configure Voice Agent and Voice Agent TTS models as the reference for the rest of the message.

{
  "type": "Settings",
  "audio": {
    "output": { "encoding": "linear16", "sample_rate": 24000 }
  },
  "agent": {
    "speak": {
      "provider": {
        "type": "open_ai",
        "model": "simba-3.2",
        "voice": "geffen_32"
      },
      "endpoint": {
        "url": "https://your-shim-host.example/v1/audio/speech",
        "headers": {
          "authorization": "Bearer placeholder-ignored-by-shim"
        }
      }
    }
  }
}

Pick a real Speechify model and voice here. The model and voice you set are forwarded to the shim, and the shim passes any name it does not recognise straight through to Speechify. The OpenAI defaults (tts-1, alloy) fall back to the shim’s compatibility aliases, simba-english and george, which work but are not the best voice on offer. Set model: "simba-3.2" and voice: "geffen_32" and Deepgram sends those verbatim, so the agent speaks with a current Simba 3.2 voice. That is the voice-quality reason this hop is worth it: Simba 3 is #1 on Artificial Analysis and the #1 real-time voice on Voice Arena, covered in WE’RE NUMBER ONE. Any voice whose models list includes simba-3.2 in GET /v1/voices is fair game.

The audio.output block is not optional, and it cost me a session to learn why. The open_ai speak provider requires uncontainerized 24 kHz linear16 output. Send container: "wav" or a 16 kHz rate and Deepgram rejects the whole Settings message with INVALID_SETTINGS: OpenAI speak provider type requires audio output format to be uncontainerized 24kHz linear16 before it speaks at all.

Does a real agent actually call the shim?

Yes, and it is worth proving rather than trusting the config. Deepgram calls your endpoint.url from its own cloud, so the shim has to be reachable on a public URL. Put a tunnel in front of the local shim:

ngrok http 8771

Then run a Voice Agent session with speak pointed at the tunnel. The demo’s agent.js opens a session with the Deepgram SDK, streams a sample WAV as the user’s turn, and configures the open_ai speak endpoint to hit the shim:

speak: {
  provider: { type: "open_ai", model: "simba-3.2", voice: "geffen_32" },
  endpoint: {
    url: `${SHIM_URL}/v1/audio/speech`,
    headers: { authorization: "Bearer placeholder-ignored-by-shim" },
  },
},

In the verified run, ngrok’s request inspector showed the proof: an inbound POST /v1/audio/speech arriving from a Deepgram cloud IP, carrying the request Deepgram built:

{ "input": "Hello! How can I help you today?", "model": "simba-3.2", "voice": "geffen_32", "response_format": "pcm" }

The shim returned 200, and the agent wrote 108,480 bytes of speech to agent-output-0.wav (a different byte count from the earlier curl smoke test because it is a different utterance and a raw PCM response rather than an MP3). That is Deepgram deciding to speak, sending simba-3.2 and geffen_32 through to the shim, and getting that exact Speechify voice back.

Run the demo

The demo wraps the verified path in one script:

git clone https://github.com/Speechify-AI/speechify-ai-demos
cd speechify-ai-demos/deepgram-voice-agent-shim
cp .env.example .env
# paste SPEECHIFY_API_KEY into .env
./run.sh

It clones tts-shims at a pinned revision into an ignored .shim/ folder, runs make openai, starts the binary, checks health, then writes shim-smoke.mp3 from the live speak call and prints speak 200 60333 bytes. DEEPGRAM_API_KEY is in .env.example for the live-agent step, but the smoke test only needs SPEECHIFY_API_KEY because it exercises the shim endpoint directly, the same way Deepgram will.

When you deploy the shim for a real agent, put it behind HTTPS, set the same environment variables there, and give Deepgram the public URL. Your users never see the Speechify key, and neither does the browser tab running the agent.

If you are choosing the voice-agent layer first, start with the Voice Agents API public beta. For latency expectations, read how we think about latency in SpeechifyAI voice agents. If you are adding TTS to code you own rather than routing through Deepgram, the Speechify Speech SDK provider and the Node.js TTS walkthrough are more direct. The full API surface is in the Speechify docs .

FAQ

Which tts-shims provider do I use for Deepgram Voice Agent?

Use the openai provider, because Deepgram’s third-party TTS call is OpenAI-shaped when provider.type is open_ai. You build make openai and the shim serves POST /v1/audio/speech. The deepgram provider in the same repo is for the opposite direction, redirecting a tool that already calls Deepgram’s own Aura Speak API to Speechify, which is not this use case.

Does Deepgram send my Speechify key to the shim?

No. Deepgram sends only the placeholder token you put in endpoint.headers.authorization. The shim reads SPEECHIFY_API_KEY from its own server environment and calls Speechify with that, so the real key lives only where the shim runs.

Do I have to change the OpenAI model and voice names?

No, the aliases work: tts-1 and alloy fall back to simba-english and george. For the recommended voice, set model: "simba-3.2" and a compatible voice such as voice: "geffen_32". The shim forwards any name it does not recognise straight through, so no shim changes are needed.

Can I run the shim in Docker?

Yes. The repo’s Dockerfile takes the provider as a build arg, so the OpenAI image build is docker build --build-arg PROVIDER=openai -t tts-shim-openai .. The binary is static (CGO_ENABLED=0) and the image uses a distroless runtime.

Where should I deploy this for production?

Anywhere that can hold a server-side environment variable and expose an HTTPS URL to Deepgram. Set SPEECHIFY_API_KEY, pick SHIM_ADDR, and keep the placeholder token in the Deepgram config. Add the usual service guardrails: logs, health checks, request timeouts, and a path to rotate the Speechify key without redeploying your agent.