Skip to main content
StandIn is hosted, so it reaches your agent from the internet. Your machine publishes two public paths, and each one has a different local process behind it. Get either the path or the target wrong and the symptom is the same: a call that connects and dies, or chat that never arrives. This is the only page that carries the mount commands. Every other page links here.

The contract

Two paths, two processes:
  • Voice is the msteams-bridge / msteams_bridge plugin. It binds 9442 on 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 msteams channel (channels.msteams.webhook.port, default 3978) 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/messages rather than anything under /msteams/.
All StandIn plugins share one port layout: calling 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:
OpenClaw and Hermes use the same two lines: the voice plugin binds 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.
Do not mount a whole port. tailscale funnel --bg --https=8443 9442 publishes every route on that port, burns one of the three funnel slots (443, 8443, 10000), and puts a port in the URL you then have to type correctly everywhere. Path mounts spend one slot for both lanes and give you portless URLs.
Confirm with tailscale funnel status before you register anything:
A registered mount is not a working mount. tailscale funnel --bg returns as soon as the rule exists, whether or not anything listens behind it. Its exit status proves nothing. Probe both paths (below) before you conclude the agent is exposed.

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:
The path is preserved either way, so the public URL is still the host plus /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

Always pass --http1.1 when probing the voice path through a funnel. Tailscale Funnel serves HTTP/2, where curl’s Connection: Upgrade headers are not a real WebSocket handshake. curl quietly sends a plain GET instead, and the server answers 404 - identical to a missing mount. People have chased that phantom for hours. The chat path is ordinary HTTP and needs no flag.
Chat, which is a plain POST:
Voice, which only answers a real upgrade:
Read the answers like this:
A plain GET is not a voice health check. OpenClaw answers 404 to a plain GET on /msteams/calling even locally, with no tunnel involved. Only an upgrade gets a meaningful answer. Never write a troubleshooting step around GET-ing the voice path.

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.