> ## Documentation Index
> Fetch the complete documentation index at: https://docs.komaa.com/llms.txt
> Use this file to discover all available pages before exploring further.

# ElevenLabs plugin

> Answer Microsoft Teams calls with an ElevenLabs agent, using the StandIn Python SDK.

`standin.plugins.elevenlabs` puts [ElevenLabs](https://elevenlabs.io/docs/agents-platform/overview) on a real Microsoft Teams call. StandIn answers the call and dials your worker; this plugin answers that dial, opens one session per call, and relays the audio both ways.

## Install and run

There is nothing to install beyond the SDK. ElevenLabs is reached over an ordinary WebSocket, so this plugin adds no dependency and needs no extra.

```bash theme={null}
pip install standin-sdk
```

```bash theme={null}
export STANDIN_SECRET=...          # your StandIn connection secret
export ELEVENLABS_API_KEY=...
export ELEVENLABS_AGENT_ID=...
python -m standin.plugins.elevenlabs
```

Expose port `9442` at the `/msteams/calling` path and register the public `wss://` URL as your StandIn identity's agent voice URL. The full walkthrough is in the [Quickstart](/python-sdk/quickstart), and there is a runnable example at [examples/elevenlabs-msteams-connector](https://github.com/komaa-com/standin/tree/main/examples/elevenlabs-msteams-connector).

## The audio format

In the ElevenLabs dashboard, set the agent's input **and** output audio format to `pcm_16000`. That is exactly what a Microsoft Teams call carries, so nothing resamples anything and the latency you measure is the model's rather than the transport's.

An agent set to anything else is refused at the first frame, with one clear log line, rather than producing a whole call of garbled audio.

## What the agent can do about the call

Declare these as **client tools** on the agent. Nothing to implement: the plugin answers them.

`client_tools()` prints the declarations, so there is nothing to retype into the dashboard:

```bash theme={null}
python -c "import json, standin.plugins.elevenlabs as e; print(json.dumps(e.client_tools(), indent=2))"
```

| Tool         | Parameters                                                            | What it does                                                                                                    |
| ------------ | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| `end_call`   | none                                                                  | Hang up.                                                                                                        |
| `express`    | `emotion`                                                             | Set the avatar's expression on the bot's tile. See [The avatar](/python-sdk/avatar#the-emotion-the-face-wears). |
| `show_image` | `url` **or** `dataBase64` + `mime`, plus `caption?` and `durationMs?` | Put a picture on the bot's video tile.                                                                          |
| `look`       | `source?`, `question?`                                                | Look at the caller's screen share or camera.                                                                    |
| `look_back`  | `question?`                                                           | Look again at something the caller has already moved past. Recorded calls only.                                 |

`show_image` is widened here, because ElevenLabs is the one provider that can hand you the bytes inline: either form will do, so neither argument is required. A URL is chosen by the model, which the caller steers, so it is fetched through the SDK's guard: public hosts only, no private or link-local addresses, and the address is re-checked at connect time to close the DNS rebind window.

<Warning>
  **The fullscreen or overlay choice is not selectable on this plugin.** The other provider plugins route `show_image` through `CallTools.dispatch`, which honours a `display` parameter. This one has its own dispatch, and it reads `mode`. `client_tools()` still emits `display` with its `fullscreen` / `overlay` enum, because that comes from the shared tool spec, so the declaration you paste will offer the model a choice that is then dropped in silence. Deleting the `display` property from the pasted declaration changes nothing except that the model stops being offered it, which is the honest version. Either way the picture goes up with the default placement. [Vision and the avatar](/python-sdk/vision#fullscreen-or-beside-your-face) covers what the two placements are for.
</Warning>

<Warning>
  `look` and `look_back` upload the frame into the ElevenLabs conversation, which **stores** the caller's screen with a third party. They therefore work only while the Microsoft Teams call is being recorded. When it is not, the agent is told why rather than being left with silence.
</Warning>

## Inside your own worker

The plugin is a `CallHandler` like any other, so you can build it yourself instead of running the module:

```python theme={null}
from standin import CallServer
from standin.plugins.elevenlabs import ElevenLabsConfig, ElevenLabsHandler

config = ElevenLabsConfig.from_env()
server = CallServer(handler_factory=lambda: ElevenLabsHandler(config))
await server.start()
```

Read the configuration **once**, then close over it. `handler_factory` runs once per call, so `handler_factory=ElevenLabsHandler` would call `ElevenLabsConfig.from_env()` again on every call, and a key removed from the environment after startup would fail the next caller rather than failing you.

`server.start()` returns as soon as the listener is bound, so a script that ends there exits before a single call arrives. `serve()`, which is what `python -m standin.plugins.elevenlabs` runs, is the sample above plus an `await asyncio.Event().wait()` to hold the process open and an `await server.aclose()` in a `finally`, which drains live calls and releases the port on the way out.

## Configuration

| Variable              | What it is                                         |
| --------------------- | -------------------------------------------------- |
| `ELEVENLABS_API_KEY`  | Your API key. Never logged, never sent to StandIn. |
| `ELEVENLABS_AGENT_ID` | Which agent answers the call.                      |

<Accordion title="Optional settings">
  | Variable                     | What it is                                                                                                                                                      |
  | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
  | `ELEVENLABS_FIRST_MESSAGE`   | Override the opening line. Applied only when the agent's security settings allow the override.                                                                  |
  | `ELEVENLABS_ENVIRONMENT`     | For an agent deployed to a named environment.                                                                                                                   |
  | `ELEVENLABS_AGENT_BRANCH_ID` | Answer with a specific branch.                                                                                                                                  |
  | `ELEVENLABS_HOST`            | Defaults to `api.elevenlabs.io`. Must be an elevenlabs.io host.                                                                                                 |
  | `ELEVENLABS_LOG_TRANSCRIPTS` | Exactly `true` to log turns. Gated a second time on the call being recorded: a transcript in your logs is a recording of the caller that they did not agree to. |
</Accordion>

<Note>
  The configuration is read once when the worker starts, not per call, so a missing key stops the worker at startup rather than surprising the first caller. `ELEVENLABS_HOST` is checked against the elevenlabs.io suffix, because your API key travels to it: a wrong host would be credential leakage rather than a failed call. The reasoning behind that shape is on [Configuration](/python-sdk/configuration#why-a-vendor-host-is-pinned).
</Note>

## Next

<CardGroup cols={2}>
  <Card title="Realtime providers" icon="bolt" href="/python-sdk/realtime-providers">
    The startup buffer, the echo guard and barge-in this plugin sits on.
  </Card>

  <Card title="Vision and the avatar" icon="eye" href="/python-sdk/vision">
    What `look`, `show_image` and `express` reach.
  </Card>
</CardGroup>
