> 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 NSMicrophoneUsageDescription This app needs microphone access for voice conversations ``` **Android.** Add permissions to `android/app/src/main/AndroidManifest.xml`: ```xml ``` ## 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 createState() => _VoiceChatState(); } class _VoiceChatState extends State { SamvaadAgent? _agent; DefaultAudioInterface? _audioInterface; bool _isConnected = false; String _transcript = ''; Future 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 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 start(AudioInputCallback inputCallback) async { // Capture audio and call inputCallback with chunks } @override Future output(Uint8List audio, {int? sampleRate}) async { // Play audio through the speaker } @override void interrupt() { // Stop queued playback } @override Future 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 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), ); ```