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

# Flutter SDK

> Use the vaani_flutter_sdk package to dispatch a Vaani voice agent and run a live WebRTC call on iOS and Android

## Overview

`vaani_flutter_sdk` dispatches a Vaani agent through the Vaani External API, then joins the room the agent was sent to over WebRTC. You give it an API key and an agent ID.

It is a Dart port of the WebRTC and dispatch parts of the [Python SDK](https://docs.vaanivoice.ai/sdk.md), with the same endpoints, field names and error types. Source: [VaaniResearchLab/vaani-flutter-sdk](https://github.com/VaaniResearchLab/vaani-flutter-sdk).

Included: call state, live transcripts, mute, speaking indicators, echo cancellation and noise suppression, and typed errors.

***

## Installation

The package is not on pub.dev yet. Add it as a git dependency:

```yaml theme={null}
dependencies:
  vaani_flutter_sdk:
    git:
      url: https://github.com/VaaniResearchLab/vaani-flutter-sdk.git
      ref: main
```

Or, with a local checkout, as a path dependency:

```yaml theme={null}
dependencies:
  vaani_flutter_sdk:
    path: ../vaani-flutter-sdk
```

**Requirements:** Flutter 3.38+, Dart 3.10+ (required by the WebRTC dependency), iOS 13.0+, Android minSdk 21+.

> [!NOTE]
>
> This package runs the live call in your app. Call history, transcripts, recordings, campaigns and agent settings are handled through the Vaani portal or your backend. See [Call History, Transcripts and Agent Settings](#call-history-transcripts-and-agent-settings).

### iOS

Add to `ios/Runner/Info.plist`:

```xml theme={null}
<key>NSMicrophoneUsageDescription</key>
<string>$(PRODUCT_NAME) uses the microphone to talk to the voice assistant.</string>
<key>UIBackgroundModes</key>
<array>
  <string>audio</string>
</array>
```

`UIBackgroundModes: audio` keeps the call running when the app goes to the background. Leave it out if the call should stop when the user leaves the app.

### Android

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

```xml theme={null}
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
```

The SDK asks for the microphone permission when the mic is first enabled.

***

## Authentication

Every request sends your API key as the `X-API-Key` header. Generate a key from [platform.vaani.dev](https://platform.vaani.dev) → **Settings → API Keys**. The key is shown only once.

```dart theme={null}
import 'package:vaani_flutter_sdk/vaani_flutter_sdk.dart';

final client = VaaniClient(apiKey: 'vaani_...');
```

| Parameter | Description | Default |
| - | - | - |
| `apiKey` | Your Vaani API key (`X-API-Key`) | *(required)* |
| `baseUrl` | Base URL of the Vaani API | `https://api.vaanivoice.ai` |
| `timeout` | Per-request timeout | `Duration(seconds: 30)` |
| `httpClient` | Your own `http.Client` (custom transport, tests) | a new `http.Client` |

Call `close()` when you are done to free the HTTP connection. If you passed your own `httpClient`, `close()` leaves it open.

```dart theme={null}
final client = VaaniClient(apiKey: 'vaani_...');
try {
  final health = await client.health();
  print(health.status);  // "healthy"
} finally {
  client.close();
}
```

You don't need a `VaaniClient` for a normal call: `VaaniVoiceSession.withApiKey` creates one and closes it in `dispose()`.

> [!WARNING]
>
> A string compiled into an iOS or Android binary can be extracted. An API key in the app is fine for prototypes and internal builds. For production, see [Keep the API key off the device](#keep-the-api-key-off-the-device).

***

## Quickstart

```dart theme={null}
import 'package:vaani_flutter_sdk/vaani_flutter_sdk.dart';

final session = VaaniVoiceSession.withApiKey(
  apiKey: 'vaani_...',
  agentId: 'd62cc9bd-2f51-4581-9f38-6c3b7d58a988',
  dispatchOptions: const WebRtcDispatchOptions(
    primaryLanguage: 'en',
    welcomeMessage: 'Hi! How can I help you today?',
  ),
);

// idle -> dispatching -> connecting -> waitingForAgent -> active
session.addListener(() => print(session.state));

// Subscribe before start() so you don't miss the greeting
session.transcripts.listen(
  (t) => print('${t.fromAgent ? "agent" : "me"}: ${t.text}'),
);

await session.start();    // dispatch + join room + enable mic
print(session.roomName);  // room the agent was dispatched into, e.g. "webrtc-1794481532-5effcd48"

await session.setMicrophoneEnabled(false);  // mute
await session.end();                        // leave the room
await session.dispose();                    // always call
```

`VaaniVoiceSession` is a `ChangeNotifier`, so it works with `ListenableBuilder`, Provider or Riverpod:

```dart theme={null}
ListenableBuilder(
  listenable: session,
  builder: (context, _) => switch (session.state) {
    VaaniVoiceSessionState.idle => const Text('Tap to talk'),
    VaaniVoiceSessionState.dispatching ||
    VaaniVoiceSessionState.connecting => const Text('Connecting…'),
    VaaniVoiceSessionState.waitingForAgent => const Text('Waiting for the assistant…'),
    VaaniVoiceSessionState.active =>
      Text(session.isAgentSpeaking ? 'Agent is speaking' : 'Listening'),
    VaaniVoiceSessionState.reconnecting => const Text('Reconnecting…'),
    VaaniVoiceSessionState.ended => const Text('Call ended'),
    VaaniVoiceSessionState.failed => Text('Error: ${session.error}'),
  },
);
```

A session is single-use: calling `start()` twice throws a `StateError`. Create a new `VaaniVoiceSession` for each conversation. The [example app](https://github.com/VaaniResearchLab/vaani-flutter-sdk/blob/main/example/lib/main.dart) has a full call screen.

***

## Voice Sessions

### Creating a Session

```dart theme={null}
// Development: API key in the app
VaaniVoiceSession.withApiKey(apiKey: key, agentId: agentId);

// Production: credentials from your backend (or any provider)
VaaniVoiceSession(credentialsProvider: myProvider);
```

`VaaniVoiceSession.withApiKey(...)` parameters:

| Parameter | Type | Default | Description |
| - | - | - | - |
| `apiKey` | `String` | required | Vaani API key |
| `agentId` | `String` | required | Agent UUID (or name) |
| `baseUrl` | `String` | `https://api.vaanivoice.ai` | Base URL of the Vaani API |
| `useSessionFlow` | `bool` | `false` | Use the session flow instead of `trigger-call`. See [Dispatch flows](#dispatch-flows) |
| `dispatchOptions` | `WebRtcDispatchOptions` | server defaults | Voice, language, greeting and prompt overrides (`trigger-call` only) |
| `gracePeriod` | `Duration?` | `null` | Reconnection window, 0–3600 s (session flow only) |
| `sessionTtl` | `Duration?` | `null` (server: 30 min) | Session lifetime, 60–86400 s (session flow only) |
| `initialMessage` | `String?` | `null` | Message injected into the agent's context at start (session flow only) |
| `sessionMetadata` | `Map<String, dynamic>` | `{}` | Key-value data forwarded to the agent (session flow only) |
| `roomOptions` | `RoomOptions` | SDK defaults | Advanced options for the underlying WebRTC room |
| `enableMicrophone` | `bool` | `true` | Publish the mic as soon as the room is joined |
| `audioOptions` | `VaaniAudioOptions` | all processing on | See [Echo cancellation and noise suppression](#echo-cancellation-and-noise-suppression) |
| `agentJoinTimeout` | `Duration?` | 30 s | Fail if no agent joins in time. `null` waits forever |

The `VaaniVoiceSession(...)` constructor takes `credentialsProvider` (required), `roomOptions`, `enableMicrophone`, `audioOptions` and `agentJoinTimeout` with the same defaults, plus:

| Parameter | Type | Default | Description |
| - | - | - | - |
| `endWhenAgentLeaves` | `bool` | `true` | End the call when the agent leaves the room (hangs up) |

### Lifecycle

```dart theme={null}
await session.start();                 // throws on dispatch or connection failure
await session.end();                   // leave the room and release the dispatch
await session.end(explicitEnd: false); // session flow: keep the reconnection grace period
await session.dispose();               // always call; ends the call if it is still running
```

`start()` completes once the room is joined, not when the agent arrives. Watch `session.state`:

| `VaaniVoiceSessionState` | Meaning |
| - | - |
| `idle` | Not started |
| `dispatching` | Asking Vaani to dispatch the agent |
| `connecting` | Joining the room |
| `waitingForAgent` | In the room; the agent has not joined yet |
| `active` | The agent is in the room; the conversation is live |
| `reconnecting` | Network dropped; reconnecting automatically |
| `ended` | Ended normally |
| `failed` | Ended because of an error; see `session.error` |

The session ends by itself when the agent leaves the room, when the room disconnects, or (session flow) when Vaani sends `session_ended`. It moves to `failed` if no agent joins within `agentJoinTimeout` or Vaani sends `agent_timeout`.

### Session Properties

| Getter | Type | Description |
| - | - | - |
| `state` | `VaaniVoiceSessionState` | Current lifecycle state |
| `error` | `Object?` | The error that moved the session to `failed` |
| `credentials` | `VaaniRoomCredentials?` | Credentials from the dispatch step |
| `roomName` | `String?` | Room the agent was dispatched into. It is also the call's `call_id` in call history. Log it: Vaani support traces calls by it |
| `agent` | participant | The agent participant, once it has joined (`null` before) |
| `isMicrophoneEnabled` | `bool` | Local mic is published and unmuted |
| `isUserSpeaking` | `bool` | Server-reported: the user is speaking now |
| `isAgentSpeaking` | `bool` | Server-reported: the agent is speaking now |
| `userAudioLevel` | `double` | Server-reported speaking level of the user, 0.0–1.0 |
| `transcripts` | `Stream<VaaniTranscript>` | Live transcription for the agent and the user |
| `sessionEvents` | `Stream<VaaniSessionEvent>` | Session WebSocket events (session flow only) |
| `room` | `Room` | The underlying WebRTC room |

`isUserSpeaking` and `userAudioLevel` are computed on the server, so they also prove the user's audio is arriving. Use them to debug "the agent can't hear me".

### Microphone

```dart theme={null}
await session.setMicrophoneEnabled(false); // mute
await session.setMicrophoneEnabled(true);  // unmute (re-applies audioOptions)
```

### Live Transcripts

```dart theme={null}
final lines = <String, VaaniTranscript>{};

session.transcripts.listen((t) {
  lines[t.segmentId] = t;  // interim updates replace earlier text for the same segment
  setState(() {});
});
```

| `VaaniTranscript` field | Description |
| - | - |
| `segmentId` | Stable for one utterance; interim and final updates share it. Key your list by it |
| `text` | Current text of the segment |
| `isFinal` | `false` while the text may still change |
| `fromAgent` | `true` for the agent, `false` for the local user |
| `participantIdentity` | Identity of the speaker in the room |

`transcripts` is a broadcast stream. Subscribe before `start()` to catch the agent's greeting. If the agent sends each transcript over two channels, the SDK keeps one and drops the duplicate.

### Using the Room Directly

`session.room` is the underlying WebRTC room. Use it for anything the session doesn't wrap, such as audio tracks, RPC, data messages and custom events. If you add your own listeners to it, dispose them yourself; the session only cleans up its own.

***

## Dispatch Flows

"Dispatch" means asking Vaani to send the agent into a room and getting back credentials to join it. There are two ways.

> [tab] trigger-call (default)
>
> One request: `POST /api/trigger-call/` with `medium: "webrtc"`. Use it for per-call voice, language or prompt overrides. `end()` leaves the room.

> [tab] Session flow
>
> Set `useSessionFlow: true`. Uses `session/create`, `session/connect` and, when the call ends, `session/disconnect`. Use it for a reconnection grace period and server-side session events. `end()` leaves the room and calls `session/disconnect`.

| | `trigger-call` | Session flow |
| - | - | - |
| Requests to start | 1 | 2 |
| Voice, language, greeting, prompt overrides | ✓ | — |
| Initial message, session TTL | — | ✓ |
| Reconnection grace period | — | ✓ |
| Session events (`agent_ready`, `agent_timeout`, `session_ended`) | — | ✓ |
| Cleanup on `end()` | Leave room | Leave room + `session/disconnect` |

### trigger-call Options

```dart theme={null}
VaaniVoiceSession.withApiKey(
  apiKey: key,
  agentId: agentId,
  dispatchOptions: const WebRtcDispatchOptions(
    primaryLanguage: 'en',
    secondaryLanguage: 'hi',
    voiceGender: VaaniVoiceGender.female,
    welcomeMessage: 'Hi! How can I help?',
    welcomeInterruptible: true,
    voiceSpeed: 1.1,
    bgNoiseEnabled: false,
    metadata: {'user_id': 'u_123'},
    modifyAgent: ModifyAgentConfig(
      persona: {'identity': {'system_prompt': 'Keep answers under two sentences.'}},
    ),
  ),
);
```

| `WebRtcDispatchOptions` | Type | Default | Description |
| - | - | - | - |
| `voiceGender` | `VaaniVoiceGender` | server: `female` | `male` or `female` |
| `primaryLanguage` | `String` | server: `hi` | Primary language code, e.g. `en`, `hi` |
| `secondaryLanguage` | `String` | server: `en` | Secondary language code |
| `welcomeMessage` | `String` | agent's greeting | Opening line spoken by the agent |
| `welcomeInterruptible` | `bool` | server default | Whether the user can interrupt the welcome message |
| `bgNoiseEnabled` | `bool` | server default | Play background noise |
| `bgNoiseVolume` | `int` | server default | Background noise volume, 0–100 |
| `voiceSpeed` | `double` | server default | Voice speed multiplier, 0.6–1.4 |
| `metadata` | `Map<String, dynamic>` | `{}` | Template variables injected into the agent's prompt |
| `modifyAgent` | `ModifyAgentConfig` | `null` | Per-call override of persona / training / experience / analysis |

Fields left `null` are not sent, so the server default applies. `bgNoiseVolume` and `voiceSpeed` outside their ranges fail an assert in debug builds and are rejected by the API with `422`.

`ModifyAgentConfig(persona:, training:, experience:, analysis:)` takes one map per section, in the same shape as the agent configuration. Sections you leave out are unchanged, and the override applies to this call only.

### Session Flow

**1. Create a session**

`session/create` returns a `sessionToken` and a shareable `sessionUrl`.

**2. Connect and get room credentials**

`session/connect` exchanges the session token for the server URL, participant token and room name. Vaani dispatches the agent at this point.

**3. Join the room**

The SDK joins the room, enables the mic and subscribes to session events.

**4. End the session**

`end()` calls `session/disconnect`. With `explicitEnd: true` (the default) the session ends now; with `false` it stays resumable for `gracePeriod`.

`VaaniVoiceSession.withApiKey(useSessionFlow: true, ...)` runs all four steps:

```dart theme={null}
final session = VaaniVoiceSession.withApiKey(
  apiKey: key,
  agentId: agentId,
  useSessionFlow: true,
  gracePeriod: const Duration(minutes: 1),   // reconnection window, 0–3600 s
  sessionTtl: const Duration(minutes: 30),   // lifetime, 60–86400 s
  initialMessage: 'User is asking about order #1234',
  sessionMetadata: {'user_id': 'user_123', 'ticket_id': 'T-1234'},
);
```

`withApiKey` always uses `ui_type: "fullpage"`. To set `widget`, build the provider yourself:

```dart theme={null}
final client = VaaniClient(apiKey: key);
final session = VaaniVoiceSession(
  credentialsProvider: SessionCredentialsProvider(
    client: client,
    agentId: agentId,
    uiType: VaaniUiType.widget,
    gracePeriod: const Duration(minutes: 1),
  ),
);
// You own this client: call client.close() after session.dispose().
```

| `SessionCredentialsProvider` | Type | Default | Description |
| - | - | - | - |
| `client` | `VaaniClient` | required | Authenticated client |
| `agentId` | `String` | required | Agent UUID or name |
| `uiType` | `VaaniUiType` | `fullpage` | Display mode: `fullpage` or `widget` |
| `gracePeriod` | `Duration?` | `null` | Reconnection window (0–3600 s), sent as seconds |
| `sessionTtl` | `Duration?` | `null` | Session lifetime (60–86400 s, server default 30 min), sent as seconds |
| `initialMessage` | `String?` | `null` | Message injected into the agent's context at start |
| `metadata` | `Map<String, dynamic>` | `{}` | Key-value data forwarded to the agent |

> [!NOTE]
>
> `gracePeriod` and `sessionTtl` outside their ranges are rejected with `422` (`VaaniValidationException`). Once a session has ended or expired, session calls fail with `410` (`VaaniSessionExpiredException`).

### Real-Time Session Events

In the session flow, `session.sessionEvents` streams events from the session WebSocket:

```dart theme={null}
session.sessionEvents.listen((e) {
  if (e.isAgentReady) print('Agent has joined the room');
  if (e.isAgentTimeout) print('Agent failed to join: ${e.raw['reason']}');
  if (e.isSessionEnded) print('Session ended: ${e.raw['reason']}');
  print(e.type);  // event name
  print(e.raw);   // full payload
});
```

| Event | `VaaniSessionEvent` getter | What the SDK does |
| - | - | - |
| `agent_ready` | `isAgentReady` | Nothing; `state` becomes `active` when the agent joins the room |
| `agent_timeout` | `isAgentTimeout` | Fails the session with `VaaniAgentTimeoutException` |
| `session_ended` | `isSessionEnded` | Ends the call |

> [!NOTE]
>
> The session-events socket is public, like `connect` and `status`: the session token in the URL is the credential. A wrong or ended token is refused, and the error arrives on `sessionEvents`. For live **call transcripts** use `session.transcripts`, not this socket.

***

## Keep the API Key Off the Device

`withApiKey` is fine for prototypes and internal builds. In production, keep the key on your backend: the backend dispatches the agent and returns only the room credentials.

```mermaid theme={null}
sequenceDiagram
  participant App
  participant BE as Your backend
  participant V as Vaani API
  App->>BE: POST /voice/start (your auth)
  BE->>V: POST /api/trigger-call/ (X-API-Key)
  V-->>BE: token, room_name, connection_url
  BE-->>App: token, room_name, connection_url
  App->>App: VaaniVoiceSession joins the room
```

**Backend** (Python, with the [Python SDK](https://docs.vaanivoice.ai/sdk.md)):

```python theme={null}
import os
from fastapi import FastAPI, Depends
from vaani_sdk import AsyncVaaniClient

app = FastAPI()
vaani = AsyncVaaniClient(api_key=os.environ["VAANI_API_KEY"])

@app.post("/voice/start")
async def start_voice(user=Depends(current_user)):   # your auth
    r = await vaani.trigger_call(
        agent_id=os.environ["VAANI_AGENT_ID"],
        medium="webrtc",
        primary_language="en",
        metadata={"user_id": user.id},
    )
    return {"connection_url": r.connection_url, "token": r.token, "room_name": r.room_name}
```

**App:**

```dart theme={null}
final session = VaaniVoiceSession(
  credentialsProvider: VaaniCredentialsProvider.custom(() async {
    final res = await myApi.post('/voice/start'); // your server calls Vaani
    return VaaniRoomCredentials(
      serverUrl: res['connection_url'],
      participantToken: res['token'],
      roomName: res['room_name'],
    );
  }),
);
```

If your backend uses the session flow, subclass `VaaniCredentialsProvider` and override `release()` so the session ends with the call:

```dart theme={null}
class MyBackendProvider extends VaaniCredentialsProvider {
  @override
  Future<VaaniRoomCredentials> fetchCredentials() async {
    final res = await myApi.post('/voice/start');
    return VaaniRoomCredentials(
      serverUrl: res['lk_url'],
      participantToken: res['lk_token'],
      roomName: res['room_name'],
      sessionToken: res['session_token'],
    );
  }

  @override
  Future<void> release(VaaniRoomCredentials c, {required bool explicitEnd}) =>
      myApi.post('/voice/end', {'session_token': c.sessionToken, 'explicit_end': explicitEnd});
}
```

`release()` runs once when the call ends, whether through `end()`, an error, the agent leaving or `dispose()`. Errors it throws are ignored, because Vaani expires sessions on its own.

`VaaniRoomCredentials`:

| Field | Type | Description |
| - | - | - |
| `serverUrl` | `String` | Room server URL (`wss://...`) |
| `participantToken` | `String` | Participant JWT |
| `roomName` | `String` | Room the agent was dispatched into |
| `sessionToken` | `String?` | Vaani session token (session flow only) |
| `liveCaptionsUrl` | `String?` | Live-captions URL, when the API returned one |
| `agentName` | `String?` | Agent name, when the API returned one |

***

## Echo Cancellation and Noise Suppression

`VaaniVoiceSession` sets the microphone's voice processing explicitly through `audioOptions`, so a dependency upgrade cannot change it silently. By default everything is on.

```dart theme={null}
VaaniVoiceSession.withApiKey(
  apiKey: key,
  agentId: agentId,
  audioOptions: const VaaniAudioOptions(
    // Some Android phones have a weak built-in echo canceller. If the agent
    // hears itself on speakerphone, force WebRTC's software one:
    echoCancellationMode: VaaniAudioProcessing.software,
  ),
);
```

| `VaaniAudioOptions` | Default | Description |
| - | - | - |
| `echoCancellation` | `true` | Acoustic echo cancellation, so the agent doesn't hear itself |
| `noiseSuppression` | `true` | Background noise suppression |
| `autoGainControl` | `true` | Levels out quiet and loud speakers |
| `highPassFilter` | `true` | Removes low-frequency rumble (fans, traffic) |
| `voiceIsolation` | `true` | Voice isolation, where the platform supports it |
| `typingNoiseDetection` | `true` | Keyboard typing-noise detection, where supported |
| `echoCancellationMode` | `automatic` | Where echo cancellation runs |
| `noiseSuppressionMode` | `automatic` | Where noise suppression runs |

| `VaaniAudioProcessing` | Effect |
| - | - |
| `automatic` (default) | Device's built-in effect when available, otherwise software |
| `platform` | iOS: Apple voice processing. Android: the phone's hardware AEC/NS |
| `software` | WebRTC's software implementation on any device |

`VaaniAudioOptions.raw()` turns everything off, for raw capture or your own audio pipeline.

> [!NOTE]
>
> The processing modes wrap an API that the WebRTC library marks experimental; the on/off flags are stable. Judge echo cancellation on a real device on speakerphone, since simulators and emulators cannot show it. On Android, `EnableBuiltInAEC(1)` and `EnableBuiltInNS(1)` in logcat confirm the hardware effects are on.

***

## Lower-Level API

`VaaniClient` calls the dispatch and session endpoints directly, without joining a room.

### Dispatch a WebRTC Call

```dart theme={null}
final client = VaaniClient(apiKey: 'vaani_...');

// Room credentials, ready to join
final creds = await client.dispatchWebRtc(
  agentId: id,
  options: const WebRtcDispatchOptions(primaryLanguage: 'en'),
);
print(creds.roomName);

// Or the full API response
final r = await client.triggerWebRtcCall(agentId: id);
print(r.token);            // participant JWT
print(r.connectionUrl);    // wss://...
print(r.liveCaptionsUrl);  // signed live-captions URL
print(r.raw);              // full response body
```

`triggerWebRtcCall` and `triggerTelephonyCall` return a `TriggerCallResponse`: `token`, `roomName`, `connectionUrl` and `liveCaptionsUrl` for WebRTC; `callId`, `roomName`, `liveCaptionsUrl`, `agentName`, `dispatchId` and `callDbId` for telephony. The full response is in `.raw`. `dispatchWebRtc` throws a `VaaniException` if the response has no token, room name or URL.

### Outbound Phone Calls

The same endpoint places phone calls. This usually belongs on a backend, but it is available:

```dart theme={null}
final call = await client.triggerTelephonyCall(
  agentId: '8cf3373e-eb6f-4b4c-9f3c-324a56a91147',
  options: const TelephonyCallOptions(
    contactNumber: '+919876543210',
    name: 'Rahul Sharma',
    metadata: {'customer_name': 'Rahul Sharma', 'property_name': 'Prateek Laurel'},
  ),
);
print(call.callId);           // "outbound-1776774443-863aa698"
print(call.liveCaptionsUrl);  // signed Server-Sent Events URL
```

| `TelephonyCallOptions` | Type | Required | Description |
| - | - | - | - |
| `contactNumber` | `String` | Yes | Phone number in E.164 format (e.g. `+919876543210`) |
| `name` | `String` | Yes | Contact display name |
| `voice` | `String` | No | Override the agent's default voice for this call |
| `metadata` | `Map<String, dynamic>` | No | Template variables injected into the agent's prompt |
| `outboundNumber` | `String` | No | Override caller ID (must be configured in Telephony settings) |
| `sipTrunkId` | `String` | No | SIP trunk to route the call through |
| `dndCheckSkipped` | `bool` | No | Skip the Do Not Disturb check |
| `bypassTrai` | `bool` | No | Allow a call after 8 PM IST (needs the workspace permission) |
| `modifyAgent` | `ModifyAgentConfig` | No | Per-call override of persona / training / experience / analysis |

### Session Endpoints

```dart theme={null}
// Create
final s = await client.sessions.create(
  agentId: id,
  uiType: VaaniUiType.widget,
  gracePeriod: const Duration(minutes: 1),
  sessionTtl: const Duration(minutes: 30),
  initialMessage: 'User is asking about order #1234',
  metadata: {'user_id': 'user_123'},
);
print(s.sessionToken);  // session JWT
print(s.sessionUrl);    // shareable URL for Vaani's hosted UI
print(s.expiresAt);

// Exchange the token for room credentials (dispatches the agent)
final conn = await client.sessions.connect(s.sessionToken);
print(conn.lkUrl);     // "wss://<your-host>"
print(conn.lkToken);   // participant JWT
print(conn.roomName);

// Check status
final status = await client.sessions.get(s.sessionToken);
print(status.status);  // "created" | "agent_ready" | "active" | "reconnecting"
// Once a session has ended or expired, this throws VaaniSessionExpiredException (410)

// Events
final sub = client.sessions.events(s.sessionToken).listen(print);
print(client.sessions.wsUrl(s.sessionToken));  // wss://.../api/webrtc/session/ws/<token>

// End
await client.sessions.disconnect(s.sessionToken);                    // starts the grace period
await client.sessions.disconnect(s.sessionToken, explicitEnd: true); // ends immediately

await sub.cancel();  // closes the socket
client.close();
```

`client.sessions.createAndConnect(...)` takes the same parameters as `create` and returns `VaaniRoomCredentials` with `sessionToken` set.

`WebRtcSessionStatus` (from `get` and `disconnect`) has `status`, `expiresAt`, `uiType`, `roomName`, `agentId`, `clientId`, `metadata` and `gracePeriodEndsAt`.

### Other Endpoints

`client.request(method, path, body:, query:)` sends an authenticated JSON request to any endpoint the SDK doesn't wrap, with the same error mapping:

```dart theme={null}
final data = await client.request('GET', '/api/health');
```

***

## Error Handling

Every error extends `VaaniException`, which carries `message` (usually the API's `detail`) and `statusCode`.

| Exception | HTTP status | Cause |
| - | - | - |
| `VaaniAuthenticationException` | 401 | Invalid or missing API key |
| `VaaniInsufficientBalanceException` | 402 | Wallet balance too low |
| `VaaniForbiddenException` | 403 | Operation not allowed for this API key (e.g. DND block on a phone call) |
| `VaaniNotFoundException` | 404 | Unknown agent, call or session |
| `VaaniSessionExpiredException` | 410 | WebRTC session has ended or expired |
| `VaaniValidationException` | 422 | Request parameters failed validation |
| `VaaniRateLimitException` | 429 | Too many requests |
| `VaaniServerException` | 5xx | Vaani server-side error |
| `VaaniTimeoutException` | - | No response within the client `timeout` (30 s) |
| `VaaniConnectionException` | - | Could not reach the API (offline, DNS, TLS) |
| `VaaniAgentTimeoutException` | - | Agent did not join within `agentJoinTimeout` (default 30 s), or Vaani sent `agent_timeout` |

```dart theme={null}
try {
  await session.start();
} on VaaniAuthenticationException {
  print('Invalid API key');
} on VaaniInsufficientBalanceException {
  print('Wallet balance is too low');
} on VaaniNotFoundException {
  print('Agent not found');
} on VaaniAgentTimeoutException {
  print('Agent did not join in time');
} on VaaniException catch (e) {
  print('API error ${e.statusCode}: ${e.message}');
}
```

`start()` throws on dispatch or connection failures. Failures after that (such as the agent timeout) don't throw: `state` becomes `failed` and `session.error` holds the exception, so check it in your listener.

***

## Troubleshooting

| Problem | What to check |
| - | - |
| The agent can't hear me | Watch `session.isUserSpeaking` while talking. If it turns `true`, audio is arriving: check the agent's language settings and prompt. If it stays `false`, check `isMicrophoneEnabled` and the app's microphone permission. On macOS, Android emulator mic input often doesn't reach the agent: test on a real phone |
| The agent hears itself | Speakerphone on a phone with a weak echo canceller. Set `echoCancellationMode: VaaniAudioProcessing.software` |
| Stuck on `waitingForAgent`, then `failed` | No agent joined within `agentJoinTimeout`. Check the agent ID and that the agent is deployed, or raise the timeout |
| Every transcript appears twice | You are listening to the room's transcription events as well as `session.transcripts`. Use only one |

When you contact Vaani support, include `session.roomName` (e.g. `webrtc-1794481532-5effcd48`). It identifies the call.

***

## Call History, Transcripts and Agent Settings

The Flutter SDK runs the live call. Everything around the call is done in the Vaani portal or from your backend:

| You want to | Where |
| - | - |
| Change the agent's prompt, voice, language or tools | The Vaani portal at [platform.vaani.dev](https://platform.vaani.dev). From the app you can only override settings for one call (`WebRtcDispatchOptions`, `modifyAgent`) |
| See call history, transcripts, recordings and summaries | The portal, or from your backend with the [Python SDK](https://docs.vaanivoice.ai/sdk.md) or the [API reference](https://docs.vaanivoice.ai/api-reference/call-history.md) |
| Look up a call made from the app | Send `session.roomName` to your backend. It is the call's `call_id` for call history, transcripts and call details |
| Run campaigns, create or delete agents | The portal, or the [Python SDK](https://docs.vaanivoice.ai/sdk.md) from your backend |
| Show the conversation during the call | `session.transcripts` |

For example, your backend can fetch a call's transcript and summary after the app reports its `roomName`:

```python theme={null}
import os
import time
from vaani_sdk import VaaniClient

with VaaniClient(api_key=os.environ["VAANI_API_KEY"]) as client:
    transcript = client.get_transcript(room_name)    # room_name sent by the app
    print(transcript.transcript)

    details = client.get_call_details(room_name)
    while details.is_processing:                     # post-call processing still running
        time.sleep(details.retry_after or 5)
        details = client.get_call_details(room_name)
    print(details.raw.get("summary"))
```

These features need your API key, so keep them on your backend, not in the app. For a one-off call to another endpoint, `client.request(method, path, body:, query:)` is available.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.