> ## 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.

# Configuration

> Full config reference for @komaa/openclaw-msteams-bridge - realtime, streaming, outbound, and security.

Config lives under `plugins.entries."msteams-voice".config` in your OpenClaw config.
**`sharedSecret` must match the secret you set in StandIn** for the hosted bridge that connects to
this plugin's media WebSocket.

## Mode selection

Set `mode` to `"realtime"` or `"streaming"`. If omitted, the runtime auto-selects **realtime** when a
realtime provider resolves, else **streaming**. Both modes honor the inbound allowlist, outbound
call-backs, recording gate, and `sessionScope` agent memory. See [Modes](/concepts/modes).

<CodeGroup>
  ```jsonc Realtime (speech-to-speech) theme={null}
  {
    "plugins": {
      "entries": {
        "msteams-voice": {
          "config": {
            "enabled": true,
            "mode": "realtime",
            "port": 9442,
            "path": "/voice/msteams/stream",
            "sharedSecret": "<same secret as in your StandIn dashboard>",
            "requireRecordingStatus": true,
            "inboundPolicy": "allowlist",
            "allowFrom": ["<caller AAD object id or phone number>"],
            "inboundGreeting": "Hello, this is the assistant.",
            "maxConcurrentCalls": 4,
            "maxDurationSeconds": 3600,
            "groupCall": {
              "requireAddress": true,
              "wakePhrases": ["assistant"],
              "followUpWindowMs": 8000
            },
            "maxVisionPerMinute": 30,
            "meetingRecap": true,
            "bilingual": true,
            "realtime": {
              "provider": "openai",
              "providers": {
                "openai": { "apiKey": "<key>", "model": "gpt-realtime" }
              },
              "instructions": "You are a helpful Teams meeting assistant.",
              "toolPolicy": "safe-read-only",
              "suppressInputDuringPlayback": true,
              "echoSuppressionWindowMs": 250,
              "echoBargeInRms": 0.02
            }
          }
        }
      }
    }
  }
  ```

  ```jsonc Streaming (STT → agent → TTS) theme={null}
  {
    "plugins": {
      "entries": {
        "msteams-voice": {
          "config": {
            "enabled": true,
            "mode": "streaming",
            "port": 9442,
            "path": "/voice/msteams/stream",
            "sharedSecret": "<same secret as in your StandIn dashboard>",
            "requireRecordingStatus": true,
            "inboundPolicy": "allowlist",
            "allowFrom": ["<caller id>"],
            "inboundGreeting": "Hello, this is the assistant.",
            "maxConcurrentCalls": 4,
            "groupCall": {
              "requireAddress": true,
              "wakePhrases": ["assistant"]
            },
            "maxVisionPerMinute": 30,
            "meetingRecap": true,
            "stt": {
              "provider": "<your-stt-provider>",
              "providers": {
                "<your-stt-provider>": { "apiKey": "<key>" }
              }
            }
          }
        }
      }
    }
  }
  ```
</CodeGroup>

In **streaming** mode, **TTS and the agent/model come from your OpenClaw configuration**. STT uses a
live transcription session - selected by `stt.provider` / `stt.providers` if set, else your
openclaw-configured transcription provider; if none resolves it falls back to VAD-segmented file
transcription. The `realtime.*` block is ignored **except** the echo-guard knobs
(`suppressInputDuringPlayback`, `echoSuppressionWindowMs`, `echoBargeInRms`), which apply in both modes.

## Outbound call-backs (optional, either mode)

```jsonc theme={null}
"outbound": {
  "enabled": true,
  "workerBaseUrl": "https://<your-standin-endpoint>",
  "tenantId": "<aad-tenant-id>",
  "answerTimeoutMs": 120000,
  "defaultMode": "notify"        // "notify" delivers a message then ends; "conversation" opens a turn
}
```

`placeCall(userObjectId, { message, mode })` is implemented on the runtime (no-answer / declined →
voicemail / no-answer); the `outbound` block enables it.

<Note>
  `workerBaseUrl` is **StandIn's outbound API URL from your dashboard** - not a server you host.
</Note>

## Key reference

| Key                                                                     | Applies                     | Meaning                                                                                                                                                              |
| ----------------------------------------------------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled`                                                               | both                        | master on/off                                                                                                                                                        |
| `mode`                                                                  | both                        | `"realtime"` \| `"streaming"` (auto if omitted)                                                                                                                      |
| `port` / `bindAddress` / `path`                                         | both                        | media WebSocket server the StandIn bridge connects to                                                                                                                |
| `sharedSecret`                                                          | both                        | HMAC secret - **must match the secret set in StandIn** (secret input)                                                                                                |
| `requireRecordingStatus`                                                | both                        | only engage once Teams reports recording active                                                                                                                      |
| `inboundPolicy`                                                         | both                        | `disabled` \| `allowlist` \| `pairing` \| `open` - **enforced** on inbound. Today `pairing` behaves **exactly like `allowlist`** in this plugin (see the note below) |
| `allowFrom`                                                             | both                        | allowlisted caller ids (Teams aadId or phone digits)                                                                                                                 |
| `inboundGreeting`                                                       | both                        | opening line                                                                                                                                                         |
| `sessionScope`                                                          | both                        | `per-phone` \| `per-call` \| `per-thread` agent-memory scope                                                                                                         |
| `maxConcurrentCalls` / `maxDurationSeconds` / `staleCallReaperSeconds`  | both                        | capacity + reaper                                                                                                                                                    |
| `groupCall.{requireAddress,wakePhrases,followUpWindowMs}`               | both                        | speak-only-when-addressed gating                                                                                                                                     |
| `maxVisionPerMinute`                                                    | both                        | vision spend cap                                                                                                                                                     |
| `meetingRecap` / `bilingual`                                            | both                        | post-call minutes / Arabic-English                                                                                                                                   |
| `realtime.{provider,providers,instructions,toolPolicy,…}`               | realtime (echo knobs: both) | realtime voice provider + behavior; provider key is a secret input                                                                                                   |
| `stt.{provider,providers}`                                              | streaming                   | live transcription provider (else openclaw STT / file fallback); provider key is a secret input                                                                      |
| `outbound.{enabled,workerBaseUrl,tenantId,answerTimeoutMs,defaultMode}` | both                        | outbound call-backs / voicemail                                                                                                                                      |

<Note>
  **What `inboundPolicy: "pairing"` actually does today.** The plugin issues no pairing codes,
  expirations, or approval prompts for inbound **calls**: with `"pairing"`, a caller is accepted only
  if their AAD object id (or phone number) is already in `allowFrom` - exactly the same check as
  `"allowlist"`. A rejected caller is logged with their caller id and the call is closed. The
  `openclaw pairing approve msteams ...` flow you may know from the **chat** channel manages chat DM
  approvals only; it does not populate this voice `allowFrom`. Until a real call-pairing flow ships,
  prefer `"allowlist"` and manage `allowFrom` yourself; `"pairing"` is accepted so such configs keep
  working with identical (allowlist) behavior.
</Note>

<Warning>
  Treat `sharedSecret` and all provider `apiKey` values as secrets - keep them out of source control.
</Warning>

## Microsoft Graph permissions

You bring your own Teams bot: register an **Azure AD app + Azure Bot resource** in your tenant,
admin-consent the required **application** permissions, and point its calling webhook at StandIn (the URL
is shown in your StandIn dashboard).

See the canonical [Graph permissions table](/teams/azure-bot#8-grant-graph-permissions) on the Create the Azure bot page.
