Put a Cartesia Line voice agent on Microsoft Teams calls. Terminates the StandIn media bridge wire protocol on one side and the Cartesia Line agent WebSocket on the other. No transcoding: the base64 PCM 16k payload is relayed verbatim both ways, barge-in via clear, native DTMF, live-context custom events, call governors with a Sonic TTS goodbye.
Project description
Microsoft Teams Bridge for Cartesia Line Agents (Python)
cartesia-msteams-bridge puts a Cartesia Line voice agent on a real Microsoft Teams call.
Prefer Node.js? The same bridge exists as an npm package:
@komaa/cartesia-msteams-bridge(repo, docs site) - same wire protocol, same environment variables, drop-in interchangeable behind the same.envfile.
The hosted StandIn media bridge (standin.komaa.com) joins the Teams call and dials into this bridge over an HMAC-authenticated WebSocket; the bridge opens one Line agent stream per call (via Cartesia's WebSocket API, the integration Cartesia provides for bringing your own telephony) and relays between them.
Microsoft Teams call
|
v
StandIn media bridge (hosted; joins the call)
| HMAC WebSocket, base64 PCM 16 kHz
v
this bridge (you run it; Python/asyncio)
| WebSocket, base64 pcm_16000
v
Cartesia Line agent (your agent code on Cartesia's platform)
The hot path is copy-only in the strongest sense: both wires carry base64 PCM 16 kHz mono, so the payload string is relayed verbatim in both directions - no decode, no re-encode, no resampling, no transcoding (frame timestamps are computed from string length + padding, not by decoding).
One wire assumption to verify on your first call. Cartesia's docs give the stream a single format config and their own quick start plays
media_outputstraight back against it, but they stop short of stating "output equalsinput_format" in writing. The bridge logs the first agent frame's byte length (and warns if it cannot be 16-bit PCM) so a format surprise is visible in the logs on your first live call instead of silently reaching the caller as noise.
Where the brain lives (read this first)
A Line agent is your code deployed on Cartesia's platform: the LLM, the tools, the conversation logic, the hangup decision. That makes this bridge deliberately a transport, and different from its siblings in two ways:
- No tool registry, no vision hooks. The Line wire has no client-side tool channel back to the bridge, so tools belong in your Line agent code. The sibling bridges (ElevenLabs, Deepgram, OpenAI) host tools in-process because those platforms route function calls back over the socket; Line does not.
- Your agent still gets the Teams context. The bridge forwards caller identity, participants, active speaker, DTMF and recording state to your agent code via the wire's
metadataandcustomevents - shapes below.
Features
- Realtime voice, end to end - the caller talks to your Line agent and hears it reply. Turn-taking and interruption are the Line platform's own; the bridge adds nothing to the latency budget beyond a relay hop.
- Barge-in mapped to Teams - when Line flushes playback (
clear), the bridge cancels queued audio on the Teams side withassistant.cancel. - Per-call personalization - caller name, tenant and direction ride the
startmetadata for your agent code, and are appended toCARTESIA_SYSTEM_PROMPTwhen you override the prompt.CARTESIA_INTRODUCTIONgives a deterministic opening line (the natural place for a spoken AI disclosure);CARTESIA_VOICE_IDoverrides the voice per call. - Native DTMF - keypad digits are forwarded as Line
dtmfevents, not prompt hacks. - Live context events - participants, active-speaker changes (rate-limited) and recording state reach your agent code as
custom{type: "call_context"}events, with an initial snapshot at stream start. - Per-call access tokens - the long-lived
CARTESIA_API_KEYnever rides an agent socket: each call mints a short-lived, agent-scoped access token and authenticates the WebSocket with that. - Two call governors - a StandIn-side cutoff the bridge speaks a goodbye for, and a bridge-side
MAX_CALL_MINUTEShard cap. WithCARTESIA_TTS_MODEL+ a voice id the goodbye is the exact text via standalone Sonic TTS (agent muted while it plays, real duration honored, frames never dropped, teardown waits for the send buffer to drain); without TTS the bridge emits agoodbye_requestcustom event your agent code may speak. Both paths are backstopped so a call can never sit open half-dead. - Observability -
GET /healthzfor liveness andGET /metrics(Prometheus text format): calls, a duration histogram, rejects, relay/drop counters, agent errors. - Hardened transport - replay-proof HMAC upgrade, single-use handshake guard, connection caps, payload caps, backpressure bounds, pre-start timeout, dead-peer detection, duplicate-call rejection, ack-gated audio, graceful drain that lets an in-progress goodbye finish, TLS 1.2 minimum on native TLS, and a
*.cartesia.aihost allowlist so your API key can only be sent to Cartesia.
Install
pip install cartesia-msteams-bridge
Python >= 3.10. One runtime dependency (aiohttp).
Quick start
1. As a CLI (env-configured)
CARTESIA_API_KEY=sk_car_... \
CARTESIA_AGENT_ID=agent_... \
WORKER_SHARED_SECRET=... \
cartesia-msteams-bridge
A .env file in the working directory is loaded automatically (existing environment wins). The package ships a fully commented .env.example.
2. As a library
import asyncio
from cartesia_msteams_bridge import load_config, start_server
async def main():
server = await start_server(load_config()) # same env variables as the CLI
try:
await asyncio.Event().wait() # run until cancelled
finally:
await server.close() # drains live calls; lets a goodbye finish
asyncio.run(main())
server.drain() ends every live call gracefully without stopping the listener; server.close() drains and stops. The CLI wires SIGTERM/SIGINT to this for you - in your own app, hook your shutdown path to server.close() so a rolling deploy never hard-drops a call.
3. Connect it to StandIn
StandIn dials in from the internet, so expose port 8080 (tunnel or public host), then register the URL on your identity in the StandIn dashboard:
tailscale funnel --bg --https=8080 8080
# Agent voice URL: wss://<machine>.<tailnet>.ts.net:8080/voice/msteams/stream
Place a Teams call to your bot (or join the sandbox meeting). StandIn joins, connects to the bridge, and your Line agent answers.
What your Line agent receives
Everything below arrives in your agent code through Cartesia's platform (custom events arrive as UserCustomSent).
Start metadata (every call):
{
"from": "msteams",
"callId": "19:meeting_...",
"callerName": "Alice W",
"tenantId": "72f9...",
"direction": "inbound"
}
Live context (custom events, advisory; one snapshot at stream start, then updates):
{ "type": "call_context", "note": "Call context snapshot at stream start.", "participantCount": 1, "recordingStatus": "unknown" }
{ "type": "call_context", "note": "There are 3 human participants on this call. Stay quiet unless directly addressed.", "participantCount": 3 }
{ "type": "call_context", "note": "The person now speaking is Sara.", "activeSpeaker": "Sara" }
{ "type": "call_context", "note": "Teams recording is now active.", "recordingStatus": "active" }
Governor goodbye request (custom event, only when no TTS goodbye is configured):
{ "type": "goodbye_request", "text": "I'm sorry, we've reached the time limit for this call. Goodbye!" }
Handle it by speaking the text; the bridge ends the call after the grace window either way.
Known limitation: transfer_call from your agent is logged and ignored - there is no phone number to transfer a Teams call to, and the StandIn wire currently has no Teams-side transfer capability. Disable transfer paths for calls with metadata.from == "msteams".
Configuration
Only the first three are required; the agent itself is configured in your Line deployment.
| Variable | Default | Meaning |
|---|---|---|
CARTESIA_API_KEY |
required | Server-side key: mints per-call access tokens and calls Sonic TTS. Never rides the agent socket. |
CARTESIA_AGENT_ID |
required | The deployed Line agent that answers calls. |
WORKER_SHARED_SECRET |
required | Shared secret from StandIn pairing; both sides must match exactly. |
CARTESIA_VOICE_ID |
unset | Override the agent's TTS voice for calls through this bridge. |
CARTESIA_INTRODUCTION |
unset | Deterministic opening line (start agent.introduction). |
CARTESIA_SYSTEM_PROMPT |
unset | Prompt override (start agent.system_prompt); caller context is appended when set. Unset = the deployed agent's prompt, untouched. |
CARTESIA_TTS_MODEL |
unset | Sonic model for the deterministic governor goodbye (e.g. sonic-2). |
CARTESIA_TTS_VOICE_ID |
unset | Voice for the goodbye TTS (falls back to CARTESIA_VOICE_ID). |
CARTESIA_TTS_LANGUAGE |
en |
Language for the goodbye TTS. |
CARTESIA_API_HOST |
api.cartesia.ai |
REST + agent WS host. Restricted to *.cartesia.ai (CARTESIA_HOST_ALLOW_ANY=true to override for a proxy you control). |
CARTESIA_VERSION |
2025-04-16 |
Cartesia-Version header sent on every request. |
MAX_CALL_MINUTES |
0 (off) |
Bridge-side hard cap per call, in minutes (fractional allowed). |
GOODBYE_TEXT |
a default line | The goodbye the bridge-side governor speaks. |
GOODBYE_GRACE_MS |
8000 |
Grace before session.end when the goodbye duration is unknown. Always hard-bounded. |
PORT / BIND |
8080 / 0.0.0.0 |
Listen address. |
TLS_CERT_PATH / TLS_KEY_PATH |
unset | PEM cert/key for native TLS (wss://, TLS 1.2+). Without both, front the bridge with a TLS terminator. |
HMAC_FRESHNESS_MS |
60000 |
Allowed clock skew for the HMAC timestamp. Must be positive. |
MAX_CONNECTIONS |
0 (= 64) |
Max concurrent worker connections. |
MAX_CONNECTIONS_PER_IP |
0 (= total) |
Per-IP cap (with TRUST_PROXY_XFF=true behind a proxy you control). |
PRE_START_TIMEOUT_MS |
0 (= 10000) |
Drop a connection that authenticates but never sends session.start. |
WORKER_IDLE_TIMEOUT_MS |
0 (= 90000) |
Dead-peer window (the worker heartbeats every 30 s). |
LOG_LEVEL |
info |
debug | info | warn | error. |
Configuration fails loud: non-numeric or negative numerics, a non-cartesia.ai host, or HMAC_FRESHNESS_MS=0 stop startup with a clear message. Setting MAX_CALL_MINUTES without a TTS goodbye logs a startup warning.
Audio formats are not configurable: the wire is PCM 16 kHz by contract and the Line stream is pinned to pcm_16000 (the copy-only property).
Disconnects and reconnects
If the worker socket drops mid-call, the bridge tears the call down: the Line stream is closed and the callId is freed. There is no mid-call re-attach: a StandIn retry with the same callId after teardown is a fresh call with a fresh agent stream and no conversation memory; a retry arriving while the old session is still live is rejected with 409 so one call can never pay for two agent streams. If the Cartesia socket drops instead (including your agent code ending the call), the bridge ends the Teams call with session.end(agent-disconnected) - there is no mid-call reconnect to Cartesia either: the connect path retries once at call start, but a steady-state Line blip ends the Teams call. A silent dead peer is detected after 90 s and the billed stream is closed.
One operational note: the per-call access token is minted with the API's maximum lifetime (1 hour) and the wire has no re-auth message. Whether Cartesia enforces expiry on established streams is not documented; set MAX_CALL_MINUTES to 55 or less to end long calls cleanly before that cliff (the bridge warns at startup otherwise).
Privacy
- The bridge relays audio and forwards call context; it logs no transcripts (the Line wire carries none) and buffers no video (
video.framemessages are dropped - there is no vision path on this wire). - Caller audio and the call metadata above transit Cartesia's platform per your Cartesia data settings; disclose the AI on the call via
CARTESIA_INTRODUCTION. - The Teams recording state is forwarded to your agent code (
call_contextevents) so your Line agent can implement its own recording-gated behavior.
Repository layout
src/cartesia_msteams_bridge/
server.py aiohttp server + WS upgrade, HMAC validation, connection guards, session registry, drain
session.py per-call relay: StandIn WS <-> Line agent WS, context events, governors, goodbye
cartesia.py Line agent socket (access tokens, start/ack, media events, keepalive), Sonic TTS
protocol.py wire message parsing (JSON, camelCase, discriminated on "type")
hmac_auth.py HMAC-SHA256("{timestampMs}.{callId}") hex, constant-time verify
config.py env config (fail-loud numeric parsing, host allowlist)
cli.py CLI entry point, .env loader, SIGTERM/SIGINT drain
examples/ runnable example project
tests/ pytest suites (fake agent port + real loopback WS; no Cartesia account needed)
Documentation
- Docs site: komaa-com.github.io/cartesia-msteams-bridge-py - getting started, the runnable example, what your Line agent receives, configuration and library API reference, wire protocol, troubleshooting.
- Example project:
examples/basic-bridge/- a runnable embedding. - StandIn (the hosted service): standin.komaa.com · docs.komaa.com.
- Cartesia Line: docs.cartesia.ai/line - deploying agents, the WebSocket API this bridge speaks.
- Siblings: the same bridge exists for ElevenLabs, LiveKit, OpenAI Realtime, and Deepgram - same wire protocol, same hardening, pick the agent platform that fits.
Contributing
PRs welcome - see CONTRIBUTING.md for local setup and conventions.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file cartesia_msteams_bridge-0.1.0.tar.gz.
File metadata
- Download URL: cartesia_msteams_bridge-0.1.0.tar.gz
- Upload date:
- Size: 55.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ff633fa36e69dcff189ff8f1e050424aa20fee6853d229a6e81573fe785c831e
|
|
| MD5 |
8d30ade2a9d4f0082996c1cc17c95d7f
|
|
| BLAKE2b-256 |
b2e3491bddffeffb18fec22c24972d6a103376ae4e3cb1d24eeeb3aa6a7e6322
|
Provenance
The following attestation bundles were made for cartesia_msteams_bridge-0.1.0.tar.gz:
Publisher:
publish.yml on komaa-com/cartesia-msteams-bridge-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cartesia_msteams_bridge-0.1.0.tar.gz -
Subject digest:
ff633fa36e69dcff189ff8f1e050424aa20fee6853d229a6e81573fe785c831e - Sigstore transparency entry: 2151972048
- Sigstore integration time:
-
Permalink:
komaa-com/cartesia-msteams-bridge-py@e02c5ec87e58e2a26287ef3b65dfd4a7f2a22835 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/komaa-com
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e02c5ec87e58e2a26287ef3b65dfd4a7f2a22835 -
Trigger Event:
push
-
Statement type:
File details
Details for the file cartesia_msteams_bridge-0.1.0-py3-none-any.whl.
File metadata
- Download URL: cartesia_msteams_bridge-0.1.0-py3-none-any.whl
- Upload date:
- Size: 43.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b3fc64791c66ee726eb0111f1ad530464f5963eb3e3760151d796ccf6e07fa71
|
|
| MD5 |
4d319cd2ec7112915054cb8a24714372
|
|
| BLAKE2b-256 |
416a9e89e0e54bdaf1621da918973c078e2e3cfe63bdd689a75afba486453ffb
|
Provenance
The following attestation bundles were made for cartesia_msteams_bridge-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on komaa-com/cartesia-msteams-bridge-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
cartesia_msteams_bridge-0.1.0-py3-none-any.whl -
Subject digest:
b3fc64791c66ee726eb0111f1ad530464f5963eb3e3760151d796ccf6e07fa71 - Sigstore transparency entry: 2151972069
- Sigstore integration time:
-
Permalink:
komaa-com/cartesia-msteams-bridge-py@e02c5ec87e58e2a26287ef3b65dfd4a7f2a22835 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/komaa-com
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@e02c5ec87e58e2a26287ef3b65dfd4a7f2a22835 -
Trigger Event:
push
-
Statement type: