Flutter SDK Implementation

Build visitor and agent communications inside a Flutter app

This guide covers visitor experiences and the separate native Agent runtime. Each uses its own short-lived token, state, and lifecycle.

Use the Flutter SDK for Android, iOS, web, desktop, native CRM, support, marketplace, banking, hotel, travel, tourism, or customer apps. It includes a Managed UI preview for fast installs and a Headless object model for custom screens while platform-specific transport details stay internal.

flutter-app

Multi-platform visitor flow

One Dart API, platform adapters where mobile needs them.

SDK token
1

App requests token

2

Backend validates user

3

Ringnity returns SDK session

Chat
AI
Voice
Video

Ringnity Support

Online now

Hi, how can we help today?
I need help with my booking.
You can continue by chat, voice, or video.
Type a message...

Overview

What this SDK is for

Use this Flutter SDK for customer-facing support experiences or internal agent workspaces. Visitor and Agent runtimes remain separate.

Visitor / Client App

This page covers customer-facing Flutter apps: chat, AI chat, internet voice call, video call, push notification, ringtone, Managed UI, and Headless UI.

Customer Backend Required

The Flutter app asks the customer backend for a short-lived SDK token. The backend stores the Ringnity Server API key and validates the app user.

Agent Runtime Included

Agent inbox, presence, Team chat, calls, push, and badges use RingnityAgent below. Admin billing and reports remain in the Admin SDK.

Server and SDK topology

Architecture that must be set up

The SDK is embedded in the Flutter app, but secret-bearing work belongs to the customer backend. This prevents developers from shipping the Ringnity Server API key inside the mobile bundle.

Flutter App

Customer app with Ringnity SDK

No Server API key
Uses short-lived SDK token

Customer Backend

Owned by the app developer

Validates app user
Stores Server API key

Ringnity Cloud

Tenant, chat, calls, AI, realtime

Issues SDK session
Processes SDK traffic
1. App requests token
2. Backend validates user
3. Backend calls Ringnity
4. SDK token returns
5. SDK opens chat/call

Do not call Ringnity Server API directly from Flutter

Flutter may call Ringnity runtime endpoints with a short-lived SDK session token. It must not contain the Server API key, platform owner credentials, or long-lived admin credentials.

Overview

Visitor capability map

This map covers the visitor runtime. The native Agent runtime is documented separately below so its token and state are never mixed with visitor sessions.

Managed Support UI

Preview

Launch a complete support experience from Flutter with Ringnity-provided widgets.

Headless Chat

Ready

Create conversations and messages from a native Flutter screen.

AI Chat

Progressive

Ask AI over HTTP and render the completed response through progressive chunk events.

Voice Call

Ready

Uses the bundled authenticated signalling transport, ICE/TURN configuration, and flutter_webrtc media engine.

Video Call

Ready

Uses bundled end-to-end WebRTC signalling, ICE queuing, camera controls, and remote media rendering.

Push and Ringtone

Ready

Register mobile push tokens and play incoming call ringtone behavior.

Package shape

The Flutter preview source package includes the Dart SDK surface, managed widgets, and adapter hooks for platform-specific mobile pieces.

text
ringnity_flutter_sdk
  Dart object model, generated operation layer, HTTP adapter,
  Managed UI preview widgets, and Headless SDK objects.

platform adapters
  Android/iOS call adapter bridge, notification/ringtone bridge,
  permissions helpers, and video view integration points.

Scalar API Reference

Use Scalar when backend developers need exact endpoint fields, schemas, auth requirements, status codes, or generated client details.

Open Scalar

Native Agent

Build a native agent workspace

Use the separate RingnityAgent root for agent inbox, direct and group Team chat, call-room control, visitor and direct Team WebRTC calls, authoritative badges, and native incoming-call actions. joinRoom connects the Ringnity-managed group-media session while the host owns its visual layout. Visitor and agent tokens never share a runtime.

Agent runtime

Dart
final nativeCalls = RingnityAgentPlatformCallAdapter();
final agent = await RingnityAgent.create(RingnityAgentConfig(
  tokenProvider: fetchFreshAgentSdkToken,
  nativeCallAdapter: nativeCalls,
));

await agent.connect();
await agent.calls.goOnline();

agent.calls.incoming.listen((call) async {
  await call.accept(video: call.type == RingnityAgentCallType.video);
});

final inbox = await agent.conversations.list(status: 'open');
final peers = await agent.team.chats.list();

Push and native calls

Dart
await agent.devices.register(
  platform: RingnityPushPlatform.ios,
  provider: RingnityPushProvider.apnsVoip,
  token: (await agent.nativeCalls.voipToken())!,
  apnsEnvironment: (await agent.nativeCalls.voipEnvironment())!,
  appId: 'com.example.support',
);

agent.nativeCalls.actions.listen((action) async {
  await agent.nativeCalls.handleAction(action);
});
await agent.badges.syncNative();

The backend must issue a short-lived Agent SDK token with the required feature scopes, including agent:devices:write when push registration is enabled. The host app owns Firebase initialization, FCM receipt, navigation, and UI.

Open Agent token and scope guide

Server

Server requirements

The backend endpoint is the trust boundary. It authenticates the customer app user, creates Ringnity context, exchanges it to a SDK session token, and returns only that short-lived token to Flutter.

Backend checklist

Ringnity tenant slug, for example your-slug.
Server API key created from the tenant dashboard.
Customer backend endpoint, for example POST /ringnity/runtime-token.
HTTPS in staging and production.
User validation in the customer backend before a token is issued.
No Server API key inside Flutter source, app bundle, logs, remote config, or compiled mobile artifacts.

Flutter compatibility

RuntimeFlutter app with Dart support
Install modeLocal path dependency during preview
Flutter SDKUse the stable Flutter version required by the host app
AndroidMicrophone, camera, notification, and network permissions when those features are used
iOSPod install, camera/microphone usage strings, and notification permission when needed
Web/DesktopSupported for SDK object model and UI surfaces where the host platform adapter is wired
NetworkHTTPS access to customer backend and Ringnity API
CallsAndroid/iOS platform call adapter for audio/video sessions
PushFCM/APNs token registration when background delivery is needed

Server

Create the runtime token endpoint

Keep secrets on your backend while Flutter receives only short-lived access. The example below creates a trusted visitor context, exchanges it for a SDK session, then returns the SDK session token.

.env
RINGNITY_API_BASE_URL=https://api.ringnity.com
RINGNITY_SERVER_API_KEY=sk_live_or_test_from_ringnity_dashboard
server.js
import express from "express";
import { RingnityServerApi } from "@ringnity/server-api";

const app = express();
app.use(express.json());

const ringnity = RingnityServerApi.create({
  apiBaseUrl: process.env.RINGNITY_API_BASE_URL,
  apiKey: process.env.RINGNITY_SERVER_API_KEY,
});

async function requireCustomer(req) {
  // Replace this with your own session, JWT, or account validation.
  return {
    id: req.body.userId,
    name: req.body.name,
    email: req.body.email,
  };
}

app.post("/ringnity/runtime-token", async (req, res) => {
  const customer = await requireCustomer(req);

  const context = await ringnity.tokens.visitorContext({
    visitor: {
      externalId: customer.id,
      name: customer.name,
      email: customer.email,
    },
    metadata: {
      source: "flutter-app",
      appVersion: req.body.appVersion,
      platform: req.body.platform,
    },
    expiresIn: 900,
  });

  res.json({
    token: context.contextToken,
    expiresIn: context.expiresIn,
    tokenType: context.tokenType,
  });
});

app.listen(3000);

Flutter receives

json
{
  "token": "eyJhbGciOi...",
  "expiresIn": 3600,
  "tokenType": "Bearer"
}

Contract

Keep the mobile response small: token, expiresIn, and tokenType. Avoid returning the Server API key, tenant internal id, credential id, scopes, or debug payloads to Flutter.

Server

Token lifecycle

Move from app entry to support session with a predictable flow. Flutter can ask the backend for a new token when the app starts, when the SDK returns unauthorized, or before a long chat/call session.

Create

Backend validates the app user and returns a short-lived SDK token.

Use

SDK sends the token as Ringnity SDK session authorization for chat, calls, AI, and push registration.

Refresh

When expired, request a new runtime token from the customer backend and recreate or refresh the SDK client.

Server

Security checklist

Use this checklist before sharing the Flutter build with a real customer.

Server API key is stored only in backend environment variables or secret manager.
Backend verifies the customer user before issuing a token.
Token endpoint is protected by HTTPS, authentication, and rate limiting.
Logs do not print full runtime token, Server API key, or Ringnity credential values.
Flutter requests microphone, camera, and notification permissions through the host app flow.
Production builds point to the production customer backend endpoint.

Flutter Setup

Download what you need

Current downloadable SDK bundle: 0.2.0-beta.0. Use the Flutter SDK for the app and the Server API SDK for the customer backend token endpoint.

Flutter Setup

Install the Flutter SDK

During preview, download the source package and install it from a local package path. When public package publishing is enabled, this section can switch to pub.dev coordinates.

1. Download and extract

Download the Flutter SDK zip, extract it beside your app workspace, then reference it from pubspec.yaml as a local path package.

Download Flutter SDK
YAML
# Pilot install from a local source package
# pubspec.yaml
dependencies:
  ringnity_flutter_sdk:
    path: ./ringnity_flutter_sdk

# Then
flutter pub get

Flutter Setup

Initialize SDK

Create the SDK from a screen, app service, provider, or support module. The token provider calls the customer backend endpoint from the Server section.

Dart
import 'dart:convert';

import 'package:http/http.dart' as http;
import 'package:ringnity_flutter_sdk/ringnity_flutter_sdk.dart';

final callAdapter = RingnityPlatformCallAdapter();

Future<RingnityRuntimeToken> fetchRingnityRuntimeToken() async {
  final response = await http.post(
    Uri.parse('https://your-backend.example.com/ringnity/runtime-token'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({
      'userId': currentUser.id,
      'name': currentUser.name,
      'email': currentUser.email,
      'appVersion': '1.0.0',
      'platform': 'flutter',
    }),
  );

  if (response.statusCode < 200 || response.statusCode >= 300) {
    throw Exception('Ringnity runtime token request failed');
  }

  final data = jsonDecode(response.body) as Map<String, dynamic>;
  return RingnityRuntimeToken(
    token: data['token'] as String,
    tokenType: data['tokenType'] as String? ?? 'Bearer',
    expiresIn: data['expiresIn'] as int? ?? 900,
  );
}

final ringnity = await Ringnity.create(
  RingnityConfig(
    apiBaseUrl: 'https://api.ringnity.com',
    tokenProvider: fetchRingnityRuntimeToken,
    callMode: RingnityCallMode.basic,
    mediaAdapter: callAdapter,
    notificationAdapter: callAdapter,
    notifications: const RingnityNotificationPreferences(
      incomingCall: RingnityRingtoneOptions(
        soundName: 'ringnity_default',
        vibrate: true,
      ),
    ),
  ),
);

Managed UI

Managed UI overview

Launch a complete Ringnity support screen with Flutter widgets. Use Managed UI when the app wants Ringnity to draw the support screen.

Dart
import 'package:ringnity_flutter_sdk/ringnity_flutter_sdk.dart';

final callAdapter = RingnityPlatformCallAdapter();

final config = RingnityConfig(
  apiBaseUrl: 'https://api.ringnity.com',
  tokenProvider: fetchRingnityRuntimeToken,
  callMode: RingnityCallMode.basic,
  mediaAdapter: callAdapter,
  notificationAdapter: callAdapter,
);

class SupportScreen extends StatelessWidget {
  const SupportScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return RingnityWidget(
      config: config,
      mode: RingnityWidgetMode.full,
      conversationSubject: 'Mobile customer support',
    );
  }
}

Custom video renderer

If the host app already owns video surfaces, inject remote and local preview widgets. Otherwise, use the default managed call surface.

Dart
RingnityWidget(
  config: config,
  mode: RingnityWidgetMode.call,
  remoteVideo: YourRemoteVideoView(),
  localPreview: YourLocalPreviewView(),
);

RingnityVideoView(
  session: callSession,
  remoteVideo: YourRemoteVideoView(),
  localPreview: YourLocalPreviewView(),
);

Managed UI

Included features

With Managed UI, the feature implementation is bundled into RingnityWidget. The host app still owns backend token flow, platform permissions, push setup, and app lifecycle.

Chat

Managed support widgets render the visitor conversation, message list, composer, and basic chat state.

AI Chat

AI surface is available when the tenant plan and AI readiness allow it.

Voice Call

Managed preview uses the SDK signalling transport and built-in flutter_webrtc media engine.

Video Call

Managed preview owns signalling, ICE queuing, media controls, and default remote/local video surfaces.

Push Notification

Host app still registers FCM/APNs; managed experiences benefit from the same device registration.

Ringtone

Managed foreground call experience can use the configured notification adapter.

Smaller managed surfaces

Use smaller managed surfaces when the app owns navigation but still wants Ringnity to render one support area, such as only Chat, AI, or Call.

Dart
RingnityWidget(config: config, mode: RingnityWidgetMode.chat);
RingnityWidget(config: config, mode: RingnityWidgetMode.aiChat);
RingnityWidget(config: config, mode: RingnityWidgetMode.call);

final ringnity = await Ringnity.create(config);

Column(
  children: [
    Expanded(child: RingnityChatWidget(ringnity: ringnity)),
    SizedBox(height: 320, child: RingnityCallWidget(ringnity: ringnity)),
  ],
);

Headless UI

Headless UI overview

Use Ringnity as the support engine behind your own Flutter screens. Headless UI is for apps that own navigation, message rendering, call controls, and analytics.

You own UI

Render message bubbles, composer, loading states, call buttons, AI response UI, and empty states in your app design.

SDK owns product logic

Use Ringnity for runtime token usage, conversation creation, message send, subscribe, AI, calls, push token, and ringtone operations.

Headless UI

Feature APIs

Each visitor feature can be used from custom Flutter screens. The same secure tokenProvider and SDK instance power every feature.

The examples below are for Headless UI. Managed UI uses the same product capabilities through RingnityWidget.

Visitor Feature

Chat

Create a visitor conversation, send messages, and keep your screen updated while support is active.

Managed UI

Use RingnityWidget mode full or chat when Ringnity should render the conversation screen.

Headless UI

Use chat.createConversation, chat.sendMessage, chat.subscribe, and stream listeners when your app owns the entire chat UI.

Dart
final readiness = await ringnity.account.readiness();

final conversation = await ringnity.chat.createConversation(
  subject: 'Need help with my order',
);

final conversationId = conversation['conversationId'] as String;

final message = await ringnity.chat.sendMessage(
  conversationId: conversationId,
  body: 'Hi, I need help.',
);

await ringnity.chat.markRead(
  conversationId: conversationId,
  messageId: message['messageId'] as String,
);

// Also available: availability, activeConversation, getConversation,
// closeConversation, updateProfile, editMessage, deleteMessage, createAttachment,
// uploadAttachment, requestCall, and finalizeCallRequest. AI also exposes routing and searchKnowledge.

final conversationEvents = ringnity.chat.conversations.listen((event) {
  renderConversation(event);
});

final messageEvents = ringnity.chat.messages.listen((message) {
  appendMessage(message);
});

final subscription = await ringnity.chat.subscribe(conversationId);

// Call these when the screen is destroyed or the conversation is closed.
subscription.unsubscribe();
await conversationEvents.cancel();
await messageEvents.cancel();

Visitor Feature

AI Chat

Ask AI and render the answer in your own component, while plan readiness and tenant configuration stay controlled by Ringnity.

Managed UI

Use Managed UI when the AI surface should appear inside Ringnity support screens.

Headless UI

Use ai.streamChat for progressive rendering after the HTTP response completes; it is not transport streaming.

Dart
await for (final event in ringnity.ai.streamChat(
  conversationId: conversationId,
  message: 'Summarize the customer issue.',
)) {
  switch (event['type']) {
    case 'ai.message.delta':
      final data = event['data'] as Map<String, Object?>;
      appendToChat(data['delta'] as String);
      break;
    case 'ai.message.completed':
      setAiAnswer(event['data'] as Map<String, Object?>);
      break;
    case 'ai.message.failed':
      showAiError(event);
      break;
  }
}

Visitor Feature

Voice Call

Start a production audio session through the bundled authenticated signalling transport and flutter_webrtc media engine.

Managed UI

Use Managed UI when Ringnity should render call controls.

Headless UI

Use calls.startAudio when your app owns the call UI; the SDK owns signalling, ICE queuing, and local/remote media lifecycle.

Dart
final permissions = await callAdapter.permissions(video: false);

if (permissions.canStartAudio) {
  final call = await ringnity.calls.startAudio(
    conversationId: conversationId,
  );

  await call.mute();
  await call.unmute();
  await call.end();
}

Visitor Feature

Video Call

Start a production video session through the bundled signalling transport, ICE/TURN configuration, and flutter_webrtc engine.

Managed UI

Use Managed UI with default call surfaces or inject custom remote/local video widgets.

Headless UI

Use calls.startVideo when your app owns call controls; the SDK owns signalling, ICE queuing, camera, and remote media lifecycle.

Dart
final permissions = await callAdapter.permissions(video: true);

if (permissions.canStartVideo) {
  final call = await ringnity.calls.startVideo(
    conversationId: conversationId,
  );

  await call.setVideoEnabled(false);
  await call.setVideoEnabled(true);
  await call.end();
}

Visitor Feature

Push Notification

Register the device push token so Ringnity can target the visitor when background delivery is needed.

Managed UI

Managed UI benefits from the same registered device token but the host app still owns FCM/APNs setup.

Headless UI

Use devices.registerPushToken after your app obtains a device token from Firebase, APNs, or a push provider.

Dart
await ringnity.devices.registerPushToken(
  platform: RingnityPushPlatform.flutter,
  provider: RingnityPushProvider.fcm,
  token: firebaseMessagingToken,
  audience: RingnityPushAudience.customer,
  externalId: 'customer-123',
  appId: 'com.example.customer',
);

Visitor Feature

Ringtone

Play or preview incoming call sounds while the app is in the foreground, then stop when the call is answered, declined, or expired.

Managed UI

Managed foreground calls can use the configured notification adapter.

Headless UI

Use notifications.previewRingtone, playIncomingRingtone, and stopRingtone when your app owns the call invitation UI.

Dart
// Add Android asset: android/app/src/main/res/raw/ringnity_default.*
// Add iOS asset: ringnity_default.caf in the app bundle.

await ringnity.notifications.previewRingtone();

await ringnity.notifications.playIncomingRingtone();

// Stop when the customer answers, declines, or the call expires.
await ringnity.notifications.stopRingtone();

Operations

Lifecycle

Keep setup predictable by treating the SDK as a runtime service owned by a support screen, app service, or provider.

1

App opens the support entry point.

2

Flutter requests a runtime token from the customer backend.

3

Customer backend validates the app user and returns a short-lived context token.

4

Flutter creates the Ringnity SDK instance with tokenProvider.

5

App chooses Managed UI or Headless UI.

6

Visitor conversation is created or restored.

7

App subscribes to updates while the screen is active.

8

Visitor sends chat, AI, voice, or video actions.

9

App unsubscribes and cancels stream listeners when leaving the screen.

10

App asks the backend for a new token when the SDK session expires.

Operations

Troubleshooting

Most integration failures come from missing tenant context, token exchange mistakes, or platform permission setup.

MISSING_TENANT

Check that the backend exchanges context with the correct tenant slug before returning the SDK session token.

SDK_ORIGIN_NOT_ALLOWED

For mobile SDK sessions, check allowed app/domain configuration and make sure the token was created for the right tenant.

401 or expired token

Request a new runtime token from the customer backend and recreate or refresh the SDK instance.

No call audio/video

Check microphone/camera permissions, platform adapter setup, and whether the host target supports the current call adapter.

No push notification

Confirm FCM/APNs token registration, appId, audience customer, and externalId.

Operations

Full example

Use Managed UI for the fastest install. Use Headless UI when the product screen must follow the host app design system.

Dart
import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:ringnity_flutter_sdk/ringnity_flutter_sdk.dart';

final callAdapter = RingnityPlatformCallAdapter();

Future<RingnityRuntimeToken> fetchRingnityRuntimeToken() async {
  final response = await http.post(
    Uri.parse('https://your-backend.example.com/ringnity/runtime-token'),
    headers: {'Content-Type': 'application/json'},
    body: jsonEncode({
      'userId': currentUser.id,
      'name': currentUser.name,
      'email': currentUser.email,
      'platform': 'flutter',
    }),
  );

  final data = jsonDecode(response.body) as Map<String, dynamic>;
  return RingnityRuntimeToken(
    token: data['token'] as String,
    tokenType: data['tokenType'] as String? ?? 'Bearer',
    expiresIn: data['expiresIn'] as int? ?? 900,
  );
}

final config = RingnityConfig(
  tokenProvider: fetchRingnityRuntimeToken,
  callMode: RingnityCallMode.basic,
  mediaAdapter: callAdapter,
  notificationAdapter: callAdapter,
);

class ManagedSupportScreen extends StatelessWidget {
  const ManagedSupportScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return RingnityWidget(
      config: config,
      mode: RingnityWidgetMode.full,
      conversationSubject: 'Mobile customer support',
    );
  }
}

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

  @override
  State<HeadlessSupportScreen> createState() => _HeadlessSupportScreenState();
}

class _HeadlessSupportScreenState extends State<HeadlessSupportScreen> {
  final messages = <Map<String, Object?>>[];
  Ringnity? ringnity;
  RingnityChatSubscription? subscription;
  StreamSubscription<Map<String, Object?>>? messageEvents;

  @override
  void initState() {
    super.initState();
    unawaited(start());
  }

  Future<void> start() async {
    final sdk = await Ringnity.create(config);
    final conversation = await sdk.chat.createConversation(
      subject: 'Mobile customer support',
    );
    final conversationId = conversation['conversationId'] as String;

    messageEvents = sdk.chat.messages.listen((message) {
      setState(() => messages.add(message));
    });

    subscription = await sdk.chat.subscribe(conversationId);

    await sdk.chat.sendMessage(
      conversationId: conversationId,
      body: 'Hi, I need help.',
    );

    setState(() => ringnity = sdk);
  }

  @override
  void dispose() {
    subscription?.unsubscribe();
    messageEvents?.cancel();
    ringnity?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return YourCustomSupportView(messages: messages);
  }
}

Last updated: June 30, 2026.

Turn Your Website Into a Real-Time Call Center

Let customers call your team directly from your website, no phone numbers and no apps required. Just add one <script>.