Skip to main content
@komaa/livekit-msteams-bridge puts a LiveKit Agent - including avatar agents (bitHuman, Tavus, and friends) - onto a Microsoft Teams call. It hosts the HMAC WebSocket that StandIn connects to, then bridges the call into a LiveKit room where your agent runs.
This is a standalone Node service, not a framework plugin. You do not install it into an agent runtime - you run the bridge and point StandIn at it. Your LiveKit agent runs as its own worker and is dispatched into a per-call room.
Prefer Python? The same bridge exists as a Python package: livekit-msteams-bridge on PyPI (pip install livekit-msteams-bridge), with its own docs site and repo. Same wire protocol, same environment variables, same hardening - pick the runtime that fits your stack. This page follows the Node package.

How it works

Per call, the bridge creates one LiveKit room, dispatches your agent into it (explicit dispatch by agentName), joins as a participant, publishes the caller’s audio, and relays the agent’s audio back to Teams. Both sides speak 16 kHz mono PCM16 - the wire protocol natively, the room via the SDK’s resampling AudioSource/AudioStream - so the bridge itself never transcodes.
Turn-taking (VAD, interruption, endpointing) runs inside your LiveKit agent session, exactly as it does for WebRTC callers. Your agent needs no Teams-specific code - it receives per-call metadata (caller name, tenant, direction, AAD id when known) and can listen on two data topics.

Requirements

  • Node.js >= 20.
  • A LiveKit server - a LiveKit Cloud project or self-hosted - with an API key and secret.
  • A LiveKit agent registered with an agent name for explicit dispatch.
  • A StandIn subscription (standin.komaa.com, free package works) - the hosted media bridge that joins the Teams call and connects to this service. See Architecture.
  • Your own Microsoft Teams bot connected to StandIn - the Teams setup walks through the Azure bot, the app package, and the upload. To try it without one, use the sandbox.

Run

Env-configured, no install step needed:
Or add it to a project (npm i @komaa/livekit-msteams-bridge) and embed it - see the library API for the programmatic surface and how to inject a fake room in tests. The bridge serves one endpoint: PORT moves the port, WS_PATH moves the base path; the defaults are 9442 and /msteams/calling, the same path OpenClaw and Hermes use, so one StandIn identity URL shape works for every backend. StandIn appends /{callId} per call, and the bridge anchors on the base path: a request outside {WS_PATH}/{callId} is rejected before the HMAC check. /healthz and /metrics stay at the root.
Voice only. This bridge has no chat lane. It serves the calling WebSocket, /healthz and /metrics, and nothing else - there is no /api/messages, so leave the Agent messages URL empty on the identity. Teams chat comes from a runtime that has its own messaging channel (OpenClaw or Hermes).

Expose the WebSocket (Agent calling URL)

StandIn connects to the bridge from the internet, so the endpoint must be reachable - a public host, or a tunnel. Expose your agent carries the mount commands, the other tunnels (cloudflared, ngrok, devtunnel), and the probes that tell a working mount from a registered one. Use the LiveKit / ElevenLabs tab: this bridge is voice only, so it needs /msteams/calling on port 9442 and no /api/messages. Your Agent calling URL is then - note there is no port:
Path routing lets a second bridge share the same machine on its own path. Whole-port mounts and the other tunnels, which publish the port rather than the path, are covered in Expose your agent.
Health check. curl http://127.0.0.1:9442/healthz returns ok when the bridge is up. Do not probe the calling path with a plain GET - it answers 404 even when healthy, because only a WebSocket upgrade is routed. And when probing through a Funnel, pass --http1.1: Funnel serves HTTP/2, where curl’s Connection: Upgrade header is not a real handshake, so curl sends a plain GET and gets the same misleading 404.

Connect it to StandIn

Register that URL (with a matching shared secret) on your identity in the dashboard, or use the sandbox to try it without your own Teams bot:
  1. Set the identity’s Agent calling URL to where the bridge listens. Leave the Agent messages URL empty - this bridge has no chat lane.
  2. Set BRIDGE_SECRET to the shared secret from pairing - both sides must match exactly, or the WebSocket handshake is rejected with 401.
  3. Place a Teams call (or join the sandbox meeting). StandIn joins, connects to the bridge, the bridge creates the room and dispatches your agent, and the agent answers.

Try the runnable examples

The repo ships two example projects so you can see a full working setup - an agent and the bridge - before wiring your own. A LiveKit call needs both: your agent runs as a worker, the bridge dispatches it into a per-call room.

Run the examples, step by step

Run the example voice agent, start the bridge pointed at it, expose the port, connect StandIn, and take a call - then swap in the bitHuman avatar agent. Docker and the agent integration points included.
Next: the full configuration reference - every environment variable, the call governor, and dispatch. Deep protocol, agent, and library docs live on the project site.

Docs site

npm

Source