The contract
Two paths, two processes:
- Voice is the
msteams-bridge/msteams_bridgeplugin. It binds9442on both OpenClaw and Hermes, and StandIn dials/msteams/calling/{callId}on it, one connection per call. - Chat is your runtime’s own Teams channel, not the voice plugin: OpenClaw’s
msteamschannel (channels.msteams.webhook.port, default3978) and the Hermes gateway’s Teams platform. That is why chat lives on a different port from voice on both runtimes, and why the path is/api/messagesrather than anything under/msteams/.
9442, managed chat 9444, Bot Framework 3978.
Voice-only? Mount /msteams/calling alone. Nothing posts chat to you, and calls work normally.
Voice-only bridges. The ElevenLabs and
LiveKit bridges have no chat lane at all - mount the voice path only.
Both default to local port
9442, and both use /msteams/calling as their public path. LiveKit
anchors on it (WS_PATH) and dials {WS_PATH}/{callId}; ElevenLabs takes the last path
segment as the call id, so it accepts whatever path you register - mount it on /msteams/calling
anyway, so one URL shape works across every runtime. Older docs and the bridge’s own startup line
mention /voice/msteams/stream; that path still works on ElevenLabs, but it is not the one to
standardize on.Mount both paths
With Tailscale Funnel, map each path onto your one public host. The proxy target must repeat the path, or the request arrives at your agent stripped and 404s:9442 on both, and chat is
3978 on both. The LiveKit and ElevenLabs bridges serve voice only and have no chat lane, so
they get one mount and no /api/messages. Their default port is 9442 too; set PORT and the
mount follows it.
Confirm with tailscale funnel status before you register anything:
Other tunnels
ngrok, cloudflared, and devtunnel forward a whole local port rather than a path, so each lane needs its own tunnel and its own public host:/msteams/calling or
/api/messages. The same ports apply on every runtime.
What you register in StandIn
Both fields live on your connection in the dashboard, and both take the same connection secret. With path mounts there is no port in either value:
Paste the bare host and StandIn appends the default path for you; paste a full URL and it is respected
as given.
Two labels, one field. The pairing console calls it Agent chat URL and the dashboard deploy
wizard calls it Agent messages URL. They are the same value, and on a bring-your-own-bot identity
that value is your
/api/messages endpoint regardless of which screen you are on.StandIn’s own endpoints are a separate layer. The
https://<identity>.standin.komaa.com/api/calling
and /api/messages addresses you point your Azure Bot at are hosted by StandIn and are not
affected by anything on this page. Those go in the Azure portal
(Create the Azure bot); the two above go in the StandIn dashboard.Probe it by hand
Chat, which is a plain POST:Managed bot chat uses a different path
Everything above describes the chat lane for an identity with your own Azure bot. If your identity uses the StandIn Managed Bot and you turn on the plugin’s managed chat lane, chat goes to the plugin’s own listener instead of your runtime’s Teams channel, on a different port and path:
In that mode your agent holds no Bot Framework credential: StandIn runs the bot and speaks a
normalized chat protocol to the plugin. The dashboard suggests the matching URL when you deploy a
managed identity, so take the value it offers rather than typing a path from memory. Voice is
unchanged either way. See Connection modes. The one-line installer with
--connection managed mounts both of these paths on Tailscale Funnel, verifies them, and prints the
values to paste: Quickstart: StandIn Managed Bot.