Quickstart for design partners
Start in 10 minutes.
A license, one install and the snippet for your stack. When the call ends, its summary shows up in your dashboard.
01
Get a license.
A license is one signed token, tenms1.…, scoped to your modules, your session limits and, for the bridge, the ElevenLabs agents it may reach. It is verified offline, so audio never waits on it.
Keep it on your servers. Browsers and devices get a 5-minute session token instead (step 03, ElevenLabs).
# Your license: keep it on your servers, never in a browser
export TENMS_LICENSE="tenms1.…"
# Where the SDK posts call summaries (your dashboard)
export TENMS_CONTROL_URL="https://app.tenms.cloud"Licenses are Ed25519-signed and verified offline by the SDK and Tenms Edge. How credentials work.
02
Install the SDK.
One Python package, CPython 3.10 or newer, CPU only. The Pipecat and LiveKit extras add the integrations. The ElevenLabs bridge and the raw Edge WebSocket need nothing in the audio path; tenms gives you the Edge client and the session-token helper.
pip install "tenms[pipecat]" # Pipecat
pip install "tenms[livekit]" # LiveKit Agents
pip install tenms # engine + Edge client + session tokensTested with pipecat-ai 1.15 and livekit-agents 1.8. The package is on PyPI from release v0.1.0; if pip cannot find it yet, ask us for a build.
03
Wire it into your stack.
Pick the one you run. Each fills slots the framework already has, so nothing else in your pipeline changes.
Focus in the transport filters, Turn in the VAD and turn strategies, Pulse through an observer. Your STT, LLM and TTS stay as they are.
# pip install "tenms[pipecat]" "pipecat-ai[daily,deepgram,openai,elevenlabs]"
import os
from pipecat.pipeline.pipeline import Pipeline
from pipecat.pipeline.runner import PipelineRunner
from pipecat.pipeline.task import PipelineTask
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import LLMContextAggregatorPair, LLMUserAggregatorParams
from pipecat.services.deepgram.stt import DeepgramSTTService
from pipecat.services.elevenlabs.tts import ElevenLabsTTSService
from pipecat.services.openai.llm import OpenAILLMService
from pipecat.transports.daily.transport import DailyParams, DailyTransport
from tenms.pipecat import TenmsCall
async def run_bot(room_url: str, token: str | None):
tenms = TenmsCall() # one per call; license from TENMS_LICENSE, focus_profile="stt"
transport = DailyTransport(room_url, token, "Tenms Bot", DailyParams(
audio_in_enabled=True, audio_out_enabled=True,
audio_in_filter=tenms.focus_filter(), # Focus: cleaned caller audio
audio_out_filter=tenms.reference_filter(), # agent playback -> echo reference
))
stt = DeepgramSTTService(api_key=os.environ["DEEPGRAM_API_KEY"])
llm = OpenAILLMService(api_key=os.environ["OPENAI_API_KEY"],
settings=OpenAILLMService.Settings(model="gpt-4o-mini"))
tts = ElevenLabsTTSService(api_key=os.environ["ELEVENLABS_API_KEY"],
settings=ElevenLabsTTSService.Settings(voice=os.environ["ELEVENLABS_VOICE_ID"],
model="eleven_flash_v2_5"))
context = LLMContext([{"role": "system", "content": "You are a helpful phone agent."}])
user_agg, assistant_agg = LLMContextAggregatorPair(context, user_params=LLMUserAggregatorParams(
vad_analyzer=tenms.vad_analyzer(), # gated VAD
user_turn_strategies=tenms.user_turn_strategies(), # Turn
))
proc = tenms.processor() # after STT, before the user aggregator
pipeline = Pipeline([transport.input(), stt, proc, user_agg, llm, tts,
transport.output(), assistant_agg])
task = PipelineTask(pipeline, observers=[tenms.observer()]) # agent text -> echo rejection
@proc.event_handler("on_call_summary")
async def on_summary(processor, summary):
print(summary["failure"], summary["quality"]) # also posted to the dashboard
await PipelineRunner().run(task) Tenms fills the VAD, turn-detector and noise-cancellation slots of AgentSession; attach() wires the echo reference and the summary.
# pip install "tenms[livekit]" "livekit-agents[deepgram,openai,elevenlabs]"
from livekit import agents
from livekit.agents import AgentSession, room_io
from livekit.plugins import deepgram, elevenlabs, openai
from tenms.livekit import TenmsAgent, TenmsLiveKit
async def entrypoint(ctx: agents.JobContext):
call = TenmsLiveKit() # license from TENMS_LICENSE, focus_profile="stt"
session = AgentSession(
stt=deepgram.STT(), llm=openai.LLM(model="gpt-4o-mini"), tts=elevenlabs.TTS(),
vad=call.vad(),
turn_handling={"turn_detection": call.turn_detector(),
"interruption": {"mode": "vad", "min_duration": 0.3}},
)
await session.start(
agent=TenmsAgent(call, instructions="You are a helpful phone agent."),
room=ctx.room,
room_options=room_io.RoomOptions(audio_input=room_io.AudioInputOptions(
noise_cancellation=call.noise_cancellation())), # Focus
)
call.attach(session) # agent speaking state, echo reference, transcripts, summary
@call.on_summary
def _(summary):
print(summary["failure"], summary["quality"])
if __name__ == "__main__":
agents.cli.run_app(agents.WorkerOptions(entrypoint_fnc=entrypoint)) Edge speaks the ElevenLabs Agents protocol. Your license must list the agent ids it may reach; the browser only ever sees a session token.
# Backend: holds the license, hands the browser a 5-minute session token.
# pip install tenms fastapi uvicorn
import os
from fastapi import FastAPI, HTTPException
from tenms.control import ControlError, amint_session_token
app = FastAPI()
@app.post("/api/voice-session")
async def voice_session(agent_id: str):
# authenticate *your* user here before minting
try:
token, expires_at = await amint_session_token(os.environ["TENMS_LICENSE"], ttl_secs=300)
except ControlError as e: # fatal: bad, revoked or expired license -> don't retry
raise HTTPException(503 if e.retryable else 403, e.code)
return {"signed_url": f"wss://edge.tenms.cloud/v1/elevenlabs/stream?agent_id={agent_id}&token={token}",
"expires_at": expires_at} One WebSocket per call, from any language: PCM16 in, cleaned audio and JSON events back. tenms.edge.EdgeClient wraps the same protocol.
# Any language works; this is Python with the plain websockets package.
import asyncio, json, websockets
EDGE = "wss://edge.tenms.cloud/v1/stream"
async def call(token, mic_frames, ref_frames, on_audio, on_event):
async with websockets.connect(EDGE, additional_headers={"Authorization": f"Bearer {token}"}) as ws:
await ws.send(json.dumps({"type": "start", "sample_rate": 16000,
"call_id": "call-123", "focus_profile": "stt"}))
ready = json.loads(await ws.recv()) # {"type": "ready", ..., "focus_profile": "stt"}
async def read(): # keep reading: Edge drops a client that stops for 10 s
async for msg in ws:
if isinstance(msg, bytes):
on_audio(msg[1:]) # 0x01 + cleaned caller audio, one 10 ms frame each
continue
ev = json.loads(msg) # speech_start, turn_end, interruption, quality, error
if ev["type"] == "summary":
return ev["summary"] # CallSummary, also posted to your dashboard
on_event(ev)
reader = asyncio.create_task(read())
await ws.send(json.dumps({"type": "agent_speaking", "speaking": True}))
for mic, ref in zip(mic_frames, ref_frames): # PCM16 LE mono at 16 kHz
if ref:
await ws.send(b"\x02" + ref) # agent playback first, as played
await ws.send(b"\x01" + mic) # caller audio
await asyncio.sleep(len(mic) / 32000) # real time (2 bytes x 16,000 per second)
await ws.send(json.dumps({"type": "transcript", "text": "hold on", "is_final": True}))
await ws.send(json.dumps({"type": "end"}))
return await reader ElevenLabs bridge, browser side: keep the ElevenLabs SDK and point it at the URL your backend returns.
// Browser: keep @elevenlabs/client; only the URL changes. WebSocket transport only
// (WebRTC goes straight to ElevenLabs and bypasses Tenms).
import { Conversation } from "@elevenlabs/client";
const res = await fetch("/api/voice-session?agent_id=" + agentId, { method: "POST" });
const { signed_url } = await res.json();
const conversation = await Conversation.startSession({
signedUrl: signed_url,
connectionType: "websocket",
});Full guides: Pipecat, LiveKit Agents, ElevenLabs bridge, Edge protocol, Python SDK.
04
Pick a Focus profile.
Leave it on stt for anything your agent transcribes. Switch only for audio people listen to. The listening profiles are never meant for STT input: full suppression raised WER in the Arena.
sttDefault- Anything your agent transcribes. Echo removal, speaker gate, gentle noise floor: the lowest measured WER.5 ms at 16 kHz
listening- Audio people hear, full noise suppression, no model.5 ms at 16 kHz
listening_miniRecordings- Recordings, QA review, listen-in. Our trained model removes about 2x the background of DSP. Edge and the Python SDK.5 ms at 16 kHz
listening_proBest listening- The best listening quality Edge offers, for recordings where 30 ms is fine. Edge only.27–30 ms
// First message on /v1/stream. Default is "stt" (audio for your STT).
// "listening", "listening_mini", "listening_pro": audio people listen to.
{"type": "start", "sample_rate": 16000, "focus_profile": "listening_mini"}
// The server echoes it back:
{"type": "ready", "focus_profile": "listening_mini", ...} from tenms.edge import EdgeClient
# Supervisor listen-in: the best listening quality Edge offers, 27-30 ms.
async with EdgeClient("wss://edge.tenms.cloud", token, sample_rate=16000,
focus_profile="listening_pro") as edge:
... import tenms
# Agent audio for your STT
engine = tenms.Engine(16000, focus_profile="stt")
# Call recordings for review: our trained model's Focus head, same 5-8 ms
review = tenms.Engine(16000, focus_profile="listening_mini") // The ElevenLabs bridge accepts stt (default) and listening only:
// its audio goes to ElevenLabs' speech recogniser.
wss://edge.tenms.cloud/v1/elevenlabs/stream?agent_id=${agentId}&token=${token}&focus_profile=stt The ElevenLabs bridge takes stt or listening only. Measurements: Focus profiles.
05
See your calls.
- Open app.tenms.cloud and sign in with your license token.
- Each call appears when it ends: quality, turns, interruptions, the failure cause and processing time.
- The overview shows call volume, failure causes, interruptions and regions for your customer only.
Edge and the ElevenLabs bridge post the summary for you. The SDK posts it when TENMS_CONTROL_URL is set; reporter=False turns that off.
curl -s "https://app.tenms.cloud/v1/calls?limit=5" \
-H "Authorization: Bearer $TENMS_LICENSE"
# -> {"calls": [...], "next_before": ..., "next_before_id": ..., "since": ...}The same data over HTTP: control-plane API, CallSummary.
06
Troubleshooting.
HTTP codes come back on the WebSocket upgrade as {"error": "<code>"}; the rest arrive as error events. EdgeClient raises them as EdgeError with .fatal and .retryable.
| Status | Code | Cause | Do |
|---|---|---|---|
| 401 | missing_licenseinvalid_license | No token, or a malformed one. Servers send Authorization: Bearer; browsers add ?token= with a session token. | Fix, don't retry |
| 401 | license_expired | The license passed its expiry, or a session token was not used within 5 minutes. | Mint a new session token |
| 403 | license_revoked | The license, or a session token's parent, was revoked. | Never retry; contact us |
| 403 | agent_not_allowed | The ElevenLabs agent id is not on your license. | Ask us to add the agent id |
| 403 | no_modules | None of the requested modules are on the license. | Request licensed modules |
| 429 | rate_limitedlicense_session_limit | More than 10 connections a second (bursts of 20), or the license's concurrent-session cap. | Back off and retry |
| 503 | at_capacitydrainingnot_ready | The replica is full, shutting down or still loading the revocation list. | Back off and retry |
| error | start_timeoutexpected_start | The first message was not a JSON start within 10 s. | Send start first |
| error | unsupported_sample_rate | Only 8, 16, 24 and 48 kHz are processed. | Resample or change the rate |
| error | profile_unavailableprofile_capacity | A listening tier this server has not loaded, or listening_pro is full on this replica. | Use listening_mini, or retry |
| error | idle_timeout | No audio for 30 s. | Send audio, or end |
The agent's own voice still reaches STT
Feed what the agent plays, as it plays: reference_filter() in Pipecat, attach() in LiveKit, 0x02 frames on Edge. The reference need not be sample-aligned, but cancelled, unplayed audio must not be fed.
Every "mm-hmm" interrupts the agent
Act only on barge_in. Tell the engine when the agent speaks (agent_speaking / set_agent_speaking); the framework integrations do this for you.
Disconnected after about 10 s
Read the socket continuously. A client that stops reading for 10 s is dropped.
Audio stalls when streaming a file
Send in real time. Edge allows 1.5x real time after a 5 s burst, then stops reading until you are back in budget.
No calls in the dashboard
SDK integrations post a summary only when TENMS_CONTROL_URL is set. Edge and the bridge post it when the call ends.
Every code: Edge error codes, upgrade refusals, bridge errors.
Stuck? Wire it in with us.
Bring your pipeline and a recording your agent got wrong. We integrate Tenms with your engineers on a call and compare the events side by side.