examples/basic-bridge
loadConfig() + startServer() in a few lines, with a custom vision hook (your own model answers
the agent’s look tool - the raw frame never leaves your process). Env-file config and graceful
shutdown are built in.What you need first
- Node.js
>= 20. - An ElevenLabs agent (Agents dashboard) with audio input and output format set to PCM 16000 Hz, plus an API key.
- A StandIn identity with its shared secret (from pairing or the dashboard). The sandbox works too if you have no Teams bot yet.
Create the ElevenLabs agent
The bridge relays audio verbatim at 16 kHz, so the one thing that must be right on the ElevenLabs side is the agent’s audio format. Create an agent in the Agents dashboard, then set:| Setting | Value | Why it matters |
|---|---|---|
| Output audio format | PCM 16000 Hz | The agent’s voice reaches Teams as-is; any other rate is garbled / wrong-pitch. This is the one people miss. |
| Input (ASR) audio format | PCM 16000 Hz | Caller audio arrives at 16 kHz. |
| Model | eleven_turbo_v2 or eleven_flash_v2 | An English agent rejects eleven_turbo_v2_5 - use turbo or flash v2. |
| Language | your call language | Set it explicitly (e.g. English) so the model choice above is validated. |
.env in the next steps:
- Agent ID (
agent_...) from the agent’s page, forELEVENLABS_AGENT_ID. - API key from dashboard -> API keys, for
ELEVENLABS_API_KEY.
Verify (or fix) the conversation audio format from the terminal
Verify (or fix) the conversation audio format from the terminal
Check the format the bridge will actually get, regardless of what the UI shows:If that is not
pcm_16000, set it directly:1. Clone and install
2. Configure the environment
.env and fill in the three required values:
| Variable | What to put there |
|---|---|
ELEVENLABS_API_KEY | Your ElevenLabs API key (server-side only; never sent to the Teams side). |
ELEVENLABS_AGENT_ID | The agent that should answer calls, from the Agents dashboard. |
WORKER_SHARED_SECRET | The shared secret from StandIn pairing - both sides must match exactly. |
PORT, MAX_CALL_MINUTES, goodbye behavior, regional hosts) has sensible
defaults - see the configuration reference.
3. Start it
4. Expose port 8080
StandIn connects from the internet, so the port needs a publicwss:// URL. Any tunnel works:
- Tailscale Funnel
- cloudflared
- ngrok
wss://<machine>.<tailnet>.ts.net:8080/voice/msteams/stream5. Connect it to StandIn and call
- In your StandIn dashboard, set the identity’s
Agent voice URL to the
wss://URL from step 4. - Make sure the identity’s shared secret equals
WORKER_SHARED_SECRET. - Place a Teams call to your bot (or join the sandbox meeting). StandIn joins, connects to the bridge, and your ElevenLabs agent answers.
The vision hook
The example ships a customVisionDescriber stub in index.mjs. When your agent calls its look
client tool, the bridge hands your function the current camera or screen-share frame and returns
your text description to the agent - the raw frame never leaves your process. Replace the stub with
a call to any vision-capable model. If you prefer configuration over code, the
VISION_API_URL / VISION_API_KEY / VISION_MODEL variables do the same against any
OpenAI-compatible endpoint - see Vision.
From example to your own project
Depend on the published package instead of the local checkout:index.mjs as your starting point. The
library API documents the
programmatic surface (config, server handle, hooks).
If something does not work
- WebSocket rejected with
401-WORKER_SHARED_SECRETdoes not match the secret in StandIn. - Bot joins the call but stays silent - the Agent voice URL is unreachable from the internet or points at the wrong port; re-check the tunnel.
- Agent audio garbled or missing - the ElevenLabs agent’s audio input/output format is not PCM 16000 Hz.
- More: Troubleshooting.