Skip to main content
This page gets a Node.js worker answering a real Microsoft Teams call. It uses the echo plugin first, on purpose: if the echo answers, your secret, your tunnel and your StandIn identity are all correct, and anything that breaks afterwards is your agent rather than the transport.

Prerequisites

  • Node.js 20 or newer
  • A StandIn identity and its connection secret, from standin.komaa.com
  • A way to expose one port over HTTPS: a tunnel on your laptop, or an ingress in a cluster
1

Install the SDK

One package. The echo plugin and the standin-echo command come with it.
2

Run the echo worker

One line tells you the listener is up:
STANDIN_HOST=127.0.0.1 binds to loopback because a tunnel is the only thing that needs to reach it on a laptop. In a container behind an ingress, leave it unset and the default 0.0.0.0 applies. The upgrade is HMAC-authenticated either way.
3

Expose port 9442

StandIn dials wss://<your-host>/msteams/calling/{callId}, so the public URL must terminate TLS and forward WebSocket upgrades on that path. The path has to match, because that is where the per-call callId segment is appended.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.
Expose only that path. Anyone who reaches the port still needs a valid signature inside the freshness window, so this is defence in depth rather than the authentication itself, but there is no reason to publish more surface than one route.
4

Register the URL and call

In the StandIn portal, set the 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.

What just happened

Replace the echo with your agent

The echo plugin is about 70 lines, and the shape of it is the shape of every plugin. Copy it and change onCallerAudio:
If your agent speaks at 24 kHz, which every realtime speech-to-speech model does, do not write your own resampler. Read Audio first: the SDK ships the resampler and the frame aligner because getting the frame boundary wrong clips the end of every turn.

Configuration

Everything reads from the environment, and CallServerOptions overrides any of it in code. That is the set a first worker needs. Configuration lists every variable the SDK reads and what reads it.

When 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. To go further without placing a call at all, see Checking the install.

Next

Call handler

The seven methods, the session object, and how to refuse a call.

Copy the echo plugin

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

Audio

What to do when your agent speaks at 24 kHz.

Security

Why a drifting clock turns every call into a 401.