> For clean Markdown of any page, append `.md` to the page URL.
> For a complete documentation index, see https://docs.sarvam.ai/llms.txt.
> For full documentation content in one file, see https://docs.sarvam.ai/llms-full.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.sarvam.ai/_mcp/server.

# Flutter

For real-time voice on iOS and Android, the Flutter SDK (`sarvamconv_ai_sdk`) wraps the [WebSocket interface](/conversations/deploy/deploy-with-code#websocket-integration) in `SamvaadAgent`, with `DefaultAudioInterface` for microphone capture and playback.

## Install

Add the SDK to `pubspec.yaml`:

```yaml
dependencies:
  sarvamconv_ai_sdk: ^1.0.0
```

```bash
flutter pub get
```

**iOS.** Add microphone permission to `ios/Runner/Info.plist`:

```xml
<key>NSMicrophoneUsageDescription</key>
<string>This app needs microphone access for voice conversations</string>
```

**Android.** Add permissions to `android/app/src/main/AndroidManifest.xml`:

```xml
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.INTERNET" />
```

## Start a voice session

```dart
import 'package:flutter/material.dart';
import 'package:sarvamconv_ai_sdk/sarvamconv_ai_sdk.dart';

class VoiceChat extends StatefulWidget {
  const VoiceChat({super.key});

  @override
  State<VoiceChat> createState() => _VoiceChatState();
}

class _VoiceChatState extends State<VoiceChat> {
  SamvaadAgent? _agent;
  DefaultAudioInterface? _audioInterface;
  bool _isConnected = false;
  String _transcript = '';

  Future<void> startConversation() async {
    _audioInterface = DefaultAudioInterface(inputSampleRate: 16000);

    final config = InteractionConfig(
      orgId: 'your_org_id',
      workspaceId: 'your_workspace_id',
      appId: 'your_app_id',
      userIdentifier: 'user123',
      userIdentifierType: UserIdentifierType.custom,
      interactionType: InteractionType.call,
      sampleRate: 16000,
    );

    _agent = SamvaadAgent(
      apiKey: 'your_api_key',
      config: config,
      audioInterface: _audioInterface,
      textCallback: (msg) async {
        if (msg is ServerTextChunkMsg) {
          setState(() => _transcript += msg.text);
        }
      },
      eventCallback: (event) async {
        if (event is ServerInteractionConnectedEvent) {
          setState(() => _isConnected = true);
        } else if (event is ServerInteractionEndEvent) {
          setState(() => _isConnected = false);
        }
      },
    );

    await _agent!.start();
    final connected = await _agent!.waitForConnect(
      timeout: const Duration(seconds: 10),
    );
    if (!connected) {
      throw Exception('Connection timeout');
    }
  }

  Future<void> stopConversation() async {
    await _agent?.stop();
    _agent = null;
    _audioInterface = null;
    setState(() => _isConnected = false);
  }

  @override
  void dispose() {
    stopConversation();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Voice Chat')),
      body: Column(
        children: [
          ElevatedButton(
            onPressed: _isConnected ? stopConversation : startConversation,
            child: Text(_isConnected ? 'Stop Voice Chat' : 'Start Voice Chat'),
          ),
          Expanded(
            child: SingleChildScrollView(
              child: Text('Transcript: $_transcript'),
            ),
          ),
        ],
      ),
    );
  }
}
```

`DefaultAudioInterface` captures the microphone and plays agent audio (16-bit PCM mono at 8 kHz, 16 kHz, or 48 kHz). `start()` fetches a signed WebSocket URL and begins streaming.

## InteractionConfig

| Field                 | Required | Description                                                                                                                                                    |
| --------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `userIdentifierType`  | Yes      | `UserIdentifierType.custom`, `.email`, `.phoneNumber`, or `.unknown`                                                                                           |
| `userIdentifier`      | Yes      | The identifier value; also what you search by in [Log Analyser](/conversations/monitor/agent-analytics/log-analyser)                                           |
| `orgId`               | Yes      | Your organization ID                                                                                                                                           |
| `workspaceId`         | Yes      | Your workspace ID                                                                                                                                              |
| `appId`               | Yes      | The agent to connect to                                                                                                                                        |
| `interactionType`     | Yes      | `InteractionType.call` for a voice session                                                                                                                     |
| `sampleRate`          | Yes      | `8000`, `16000`, or `48000` (16-bit PCM mono)                                                                                                                  |
| `version`             | No       | Pins a specific committed agent version. If omitted, the SDK uses the latest committed version, and the connection fails if the agent has no committed version |
| `agentVariables`      | No       | Seed [agent variables](/conversations/build/variables-personalization) at session start                                                                        |
| `initialLanguageName` | No       | Starting language; must be one of the agent's allowed languages (for example `SarvamToolLanguageName.hindi`)                                                   |
| `initialStateName`    | No       | Starting state, if the agent uses [states](/conversations/build/states-conversation-flow)                                                                      |
| `initialBotMessage`   | No       | First message from the agent                                                                                                                                   |

## Callbacks

Pass any of these to `SamvaadAgent` to react to what happens during the call:

| Callback        | Fires with                               | Use it for                                                                                                                                                       |
| --------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `textCallback`  | `ServerTextMsg` or `ServerTextChunkMsg`  | Streaming or complete agent text                                                                                                                                 |
| `audioCallback` | audio chunk (`audioBytes`, `sampleRate`) | Raw agent audio, if you're handling playback yourself instead of using `audioInterface`                                                                          |
| `eventCallback` | session event                            | `ServerInteractionConnectedEvent`, `ServerUserInterruptEvent`, `ServerInteractionEndEvent`, speech start/end, language change, state transition, variable update |

## DefaultAudioInterface

```dart
final audioInterface = DefaultAudioInterface(
  inputSampleRate: 16000,
  outputSampleRate: 16000,
);
```

To bring your own I/O, implement `AudioInterface`:

```dart
class CustomAudioInterface implements AudioInterface {
  @override
  Future<void> start(AudioInputCallback inputCallback) async {
    // Capture audio and call inputCallback with chunks
  }

  @override
  Future<void> output(Uint8List audio, {int? sampleRate}) async {
    // Play audio through the speaker
  }

  @override
  void interrupt() {
    // Stop queued playback
  }

  @override
  Future<void> stop() async {
    // Cleanup
  }
}
```

## Session methods

| Method                                                       | Description                                       |
| ------------------------------------------------------------ | ------------------------------------------------- |
| `await agent.start()`                                        | Fetch a signed WebSocket URL and connect          |
| `await agent.waitForConnect(timeout: Duration(seconds: 10))` | Wait until connected (returns `false` on timeout) |
| `await agent.sendAudio(audioBytes)`                          | Send raw 16-bit PCM mono at `config.sampleRate`   |
| `agent.isConnected`                                          | Current connection status                         |
| `agent.interactionId`                                        | The current interaction (call) ID, once connected |
| `await agent.waitForDisconnect()`                            | Wait until the session ends                       |
| `await agent.stop()`                                         | Close the connection and clean up                 |

> **Note**
>
> Call `await agent.stop()` in `dispose()` so the WebSocket and audio interface are cleaned up. Reconnection is not supported: each WebSocket URL is single-use. If the connection drops, call `stop()` and create a new `SamvaadAgent`.

Stop the agent when the app goes to the background:

```dart
class _MyWidgetState extends State<MyWidget> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _agent?.stop();
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) {
      _agent?.stop();
    }
  }
}
```

## Troubleshooting

**Microphone permission denied.** Request the microphone before starting:

```dart
import 'package:permission_handler/permission_handler.dart';

final status = await Permission.microphone.request();
if (!status.isGranted) {
  // Ask the user to enable the permission
}
```

**No audio output.** Check device volume, that `DefaultAudioInterface` is initialized, and that `sampleRate` matches the session config.

**Connection timeout.** Check connectivity, API key, `orgId` / `workspaceId` / `appId`, and that the agent has a committed version.

> **Warning**
>
> Never embed your API key in the app binary. Omit `apiKey`, set `baseUrl` to a backend you control, and pass your app auth in `headers`:

```dart
final agent = SamvaadAgent(
  config: config,
  baseUrl: 'https://your-proxy-server.com/sarvam-proxy/',
  headers: {
    'Authorization': 'Bearer user_session_token',
  },
  audioInterface: DefaultAudioInterface(inputSampleRate: 16000),
);
```