Skip to main content
standin-sdk is the worker side of a StandIn call. StandIn is the hosted bridge that joins the Microsoft Teams call: it owns the Microsoft side entirely, the bot registration, Graph, media negotiation and the avatar tile, and it talks to your worker over one authenticated WebSocket per call. This package is that socket’s other end. You write a handler. The SDK does everything else on the wire.
One package, imported as standin, and every plugin ships inside it. That line alone is already enough for ElevenLabs, Deepgram and Cartesia: they are reached over an ordinary WebSocket, so they need nothing beyond aiohttp. A capability lands once in the core and reaches every plugin, which is why there is no second package to keep in step.

The four extras

An extra is either a framework that runs inside your process or a capability that needs a wheel most deployments do not want. Both kinds are off by default, and the two capability extras are designed to stay off rather than to fail. pip install "standin-sdk[all]" takes all three wheels at once. Plugins are resolved on first touch and never at import standin, so the base install is aiohttp and nothing else. standin.plugins.livekit is the stable spelling, because the path says which layer a name comes from; standin.livekit works as a shorthand and is not the documented one.

How it fits together

One call is one socket. StandIn dials wss://<your-host>/msteams/calling/{callId}, your CallServer answers it, verifies the handshake, and builds one handler for that call. When the call ends, the handler is closed and the slot is freed.

What the SDK owns, and what you own

Everything that is the same whichever agent framework you use belongs to CallServer. That split is why a StandIn plugin is small. A handler never tracks a sequence number, never builds a frame, and never sees a signature.

The three things you import

CallServer

Answers the dial, authenticates it, speaks the wire protocol, drives one handler per call.

CallHandler

The five-method seam your plugin implements. Every method is optional.

ChatChannel

The Microsoft Teams messages lane. Dialed out from your worker, so chat needs no listener and no bot credential.

The whole contract

CallHandler is a typing.Protocol, not a base class. Nothing inherits from anything, a missing method is a no-op, and a synchronous method is accepted where there is nothing to await.

The whole surface, by lane

Those three names are the seam. Everything else in the package is a lane you can ignore until you need it. Every name below resolves from import standin unless the row says otherwise, and the page in the last column is the lane’s page rather than a per-name reference: it is where the reasoning lives, and this table is how you find which page that is.

The two SDKs are one API

The wire protocol, the audio format and the HMAC are byte-identical between the languages, and shared conformance vectors prove it rather than promising it: both protocol modules are generated from one schema and gated against drift. The core seam is the same in both. Same handler methods, same session, same watchdogs, same defaults. A handler ported between the languages changes only its method names: Python is snake_case, TypeScript is camelCase. Around the seam a few peripheral helpers exist in one language only, and the page each one belongs to says so. Three that matter here:
  • Timers are seconds in Python and milliseconds in TypeScript. pre_start_timeout=10.0 against preStartTimeoutMs: 10000. The names differ too, so a straight port does not compile, but a configuration file copied between two workers does not complain.
  • The two optional video callbacks sit differently. on_video_frame and on_speaker_change live on separate VideoHandler and SpeakerHandler protocols here, because CallHandler is runtime_checkable and a sixth member would break isinstance for every handler written before the vision lane existed. In TypeScript every handler member is optional, so they are simply two more of them.
  • Who may call the agent is a TypeScript module. isInboundCallAllowed and its companions ship in that SDK only. A Python worker writes the same check in its own handler, which is the refusing a call snippet.

Why the plugin lists differ per language

Most plugins exist in both languages, and the ones that do not are settled by the same rule: which language a plugin lives in is decided by the framework it integrates, not by preference. ElevenLabs, Deepgram, Cartesia and LiveKit are in both trees. Hermes Agent has to be Python, because Hermes loads the adapter into its own process. OpenClaw is TypeScript for the same reason in reverse, and OpenAI Realtime is a TypeScript plugin today. Nothing is missing from either side of the wire: both SDKs speak the same protocol, run the same conformance vectors, and answer the same calls.

Where the SDK stops

Three boundaries, and each is a real line rather than a disclaimer. Speech and reasoning are your framework’s. Speech recognition, speech generation and the model that decides what to say belong to your provider. The SDK carries PCM in both directions and gives you the turn-taking pieces if you are assembling three vendors yourself, but nothing in this package transcribes, synthesizes or thinks. The Microsoft side is StandIn’s. Bot registration, Graph, the media negotiation and the call itself. Your worker never holds a Bot Framework credential and never talks to Microsoft. StandIn draws the avatar tile; the SDK sends it hints. This is the boundary most often described wrongly, so it is worth stating exactly. The SDK does carry the vision and avatar lane: session.express names an emotion, session.send_speech_marks sends the viseme timeline that drives lip-sync, session.display_image puts a picture on the tile, session.send_tile_frame puts one frame of your own video on it, and session.latest_video_frame and CallHandler.on_video_frame are how the caller’s camera and screen share reach you. Whole modules back those: avatar, lipsync, vision, vision_tools, tile and ambient, all on the barrel above. What the SDK does not do is render anything. It never composites a face, never encodes the avatar and never knows how the tile is drawn. See Vision and the avatar and The avatar. A message type this SDK does not recognise is ignored by contract, which is what lets an older worker and a newer StandIn interoperate. That rule is about unknown frames, not about the avatar: those are frames the SDK itself sends.

Audio format

PCM16, 16 kHz, mono, little-endian, in both directions. on_caller_audio receives it and send_audio expects it back. Realtime models usually speak 24 kHz, which is why the SDK ships a resampler and a frame aligner. See Audio.

Requirements

  • Python 3.10 or newer
  • aiohttp 3.9 or newer, the only runtime dependency
  • A StandIn identity and its connection secret, from standin.komaa.com
  • A public wss:// route to your worker, so StandIn can dial in

Next

Quickstart

Install the package, run the echo plugin, and hear yourself on a real Microsoft Teams call.

Call handler

The five methods, one at a time, with a full worked handler.

Audio helpers

resample_pcm16, FrameAligner, and the 16 kHz against 24 kHz problem.

Security

The HMAC handshake, the two signing lanes, and what the SDK refuses.