---
sidebar_position: 2
title: Quickstart
description: A quickstart guide to making and receiving VoIP calls using the Firetell Flutter SDK.
---

# Quickstart Guide

This guide walks through the core workflows of the Firetell Flutter SDK: connecting, making outbound calls, handling incoming calls (foreground + push), and controlling active calls.

---

## Step 1: Initialize the Client

Create a `FiretellClient` with your agent JWT and workspace domain. Wait for the `ready` future to resolve before making calls.

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

final client = FiretellClient(
  jwt: 'your-agent-jwt-token',
  domain: 'yourcompany.firetell.app',
);

try {
  final session = await client.ready;
  print('Authenticated as: ${session.username}');
} catch (e) {
  print('Initialization failed: $e');
}
```

During initialization, the SDK:
1. Decodes the JWT to extract agent identity
2. Fetches workspace metadata (ICE servers, WS URLs) via REST
3. Connects the SSE real-time event stream
4. Caches ICE servers locally for push-originated calls

---

## Step 2: Listen to Real-time Events (SSE)

Subscribe to streams on the client to respond to incoming calls, agent status updates, and connection state changes.

```dart
// Connection state
client.onConnectionState.listen((state) {
  print('SSE: ${state.name}'); // connecting, connected, disconnected, reconnecting
});

// Incoming call ring notification (shows caller info before WebRTC connects)
client.onCallRing.listen((CallRingParams params) {
  print('Incoming: ${params.callerName} (${params.callerNumber})');
  // → Show Ringing UI & play ringtone
});

// Incoming call WebRTC offer ready to answer
client.onCallOffer.listen((Call call) {
  print('Call offer ready: ${call.callId}');
  // → Show Answer / Decline buttons
});

// Agent state changes in the workspace
client.onAgentState.listen((data) {
  print('Agent ${data['username']} is now ${data['state']}');
});

// Errors
client.onError.listen((error) {
  print('Client error: $error');
});
```

---

## Step 3: Make an Outbound Call

Use `makeOutboundCall()` to initiate a call. The SDK handles WebRTC setup, Full ICE gathering, REST API call, and WebSocket signaling automatically.

```dart
final call = await client.makeOutboundCall(
  to: '+84901234567',   // Phone number, extension, or SIP URI
  from: '84281234567',  // Optional: outbound caller ID
  isVideo: false,
);

// Monitor call lifecycle
call.onStateChange.listen((event) {
  print('State: ${event.state}'); // initiated → ringing → active → ended

  if (event.state == CallState.ended ||
      event.state == CallState.error ||
      event.state == CallState.cancel) {
    print('Call terminated. Reason: ${event.reason}');
  }
});

// Remote audio stream (plays automatically for audio calls)
call.onRemoteStream.listen((stream) {
  if (stream != null) {
    print('Remote audio stream received');
  }
});
```

---

## Step 4: Handle Incoming Calls

### Foreground (SSE)

When the app is in the foreground, incoming calls arrive via the SSE stream:

```dart
client.onCallRing.listen((params) {
  // Show incoming call UI with caller info
  showIncomingCallUI(params.callerName, params.callerNumber);
});

client.onCallOffer.listen((call) async {
  // User taps "Answer"
  await call.accept();
  // Audio is now flowing ✅

  // User taps "Decline"
  // await call.reject();
});
```

### Background / Killed (VoIP Push)

When the app is in the background or terminated, calls arrive via push notifications:

```dart
// main.dart — top-level background handler
@pragma('vm:entry-point')
Future<void> _backgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();

  if (message.data['event'] == 'call.ring') {
    final params = CallRingParams.fromFcmData(message.data);

    // Show native CallKit (iOS) / full-screen notification (Android)
    await FlutterCallkitIncoming.showCallkitIncoming(CallKitParams(
      id: params.callId,
      nameCaller: params.callerName,
      handle: params.callerNumber,
      extra: params.toMap(), // ← stores ws_url + call_token for answer handler
    ));
  }
}
```

When the user answers from the lock screen:

```dart
// Uses ws_url + call_token from push payload directly — no FiretellClient needed
FlutterCallkitIncoming.onEvent.listen((event) async {
  if (event?.event == Event.actionCallAccept) {
    final extra = (event!.body as Map)['extra'] as Map<String, dynamic>;
    final params = CallRingParams.fromMap(extra);
    final iceServers = await IceServerCache.load(); // cached from last session

    final call = Call(iceServers: iceServers);
    call.callId = params.callId;
    await call.connectSignaling(params.wsUrl!, params.callToken);
    await call.accept();
    // Audio flows ✅
  }
});
```

For complete push notification setup, see [VoIP Push Notifications](voip-push).

---

## Step 5: Call Controls

```dart
// Mute / Unmute
await call.mute();
await call.unmute();
await call.toggleMute();

// Hold / Unhold (SDP renegotiation)
await call.onhold();
await call.unhold();

// DTMF digits
call.sendDTMF('1');
call.sendDTMF('#');

// Transfer
await call.transfer('+1987654321', reason: 'Customer request');

// Hang up
await call.hangup();
```

---

## Step 6: Register Push Tokens

Register push tokens after login so the backend can send VoIP push notifications for incoming calls:

```dart
final deviceId = await DeviceIdHelper.getOrCreate();
final fcmToken = await FirebaseMessaging.instance.getToken();

await PushTokenService.registerVoipPushToken(
  baseUrl: client.baseUrl,
  jwt: client.jwt,
  pushToken: fcmToken!,
  deviceId: deviceId,
  platform: Platform.isIOS ? 'ios' : 'android',
);
```

---

## Step 7: Logout

```dart
// Unregister push tokens + cleanup
await PushTokenService.logout(
  baseUrl: client.baseUrl,
  jwt: client.jwt,
  deviceId: await DeviceIdHelper.getOrCreate(),
);

// Destroy client (hangs up active calls, closes SSE)
client.destroy();
```
