Skip to main content
The echo plugin sends your voice back. Run it first, before you involve an agent: if the echo answers, your secret, your tunnel and your StandIn identity are all correct, and anything that breaks afterwards is your agent.

Prerequisites

  • Python 3.10 or newer
  • A StandIn identity and its connection secret, from standin.komaa.com
  • A way to expose one local port over HTTPS and WebSocket: a tunnel such as Tailscale Funnel or ngrok, or an ingress in front of a container

Step 1: install

That is the whole install. The echo plugin used below is part of the package, so there is nothing else to add until you pick a framework.

Step 2: set the secret

STANDIN_SECRET is the only required variable. It is what the handshake signature is checked against, and it is what arms the listener at all: without it, CallServer refuses to construct.
STANDIN_HOST=127.0.0.1 keeps the listener off every other interface while a local tunnel is the only thing reaching it. The default is 0.0.0.0, which is what you want in a container behind an ingress. The upgrade is HMAC-authenticated either way. Those six are all the call and chat lanes read. The other lanes have their own, and Configuration is the one page that lists every STANDIN_ variable with what owns it. Configuration is environment-only by design, so credentials never reach a source file and the plugins all read their keys the same way.

Step 3: run the echo plugin

You should see one line naming the address, the port and the path it is answering on. The process now waits for a call and does nothing else.

Step 4: expose the port

StandIn dials your worker, so the worker needs a public wss:// address with the /msteams/calling path routed to port 9442. Expose your agent has the mount command for Tailscale Funnel, ngrok, cloudflared and devtunnel, and the probes that tell a registered mount from a working one. It is the only page that carries those commands, because three divergent spellings of one URL across three pages is what caused a live incident here. Terminate TLS at your public ingress. The path has to match, because that is where the per-call callId segment is appended: StandIn dials wss://<your-host>/msteams/calling/{callId}.
Expose only that path, and keep the secret out of version control. Anyone who can reach the port still needs a valid signature within the freshness window, but there is no reason to publish more surface than one route.

Step 5: register the URL and call

In the StandIn portal, set your identity’s agent voice URL to the public wss:// URL, including the /msteams/calling path. Then call the identity from Microsoft Teams and talk. You should hear yourself, delayed by the round trip. The worker logs the call id and the caller’s display name when the call starts, and the close reason when it ends.

What just happened

If it does not answer

GET /healthz on the same port returns {"ok": true, "calls": <n>}, which is the cheapest way to confirm the listener is really bound.

Next

Write a handler

Replace the echo with your own agent loop.

Copy the echo plugin

The template, and how to turn it into your own plugin.

LiveKit

Two StandIn-specific lines in an ordinary LiveKit agent file.

Hermes Agent

Your Hermes agent, answering the phone in-process.