Flutter SDK
The official Flutter SDK (uchara_sdk) for embedding realtime chat into Flutter
applications. It is headless — it provides no prebuilt UI — so you build the
widget experience yourself and drive it with the SDK’s session, conversation,
message, upload, and realtime APIs.
Requirements
Section titled “Requirements”- Dart
>=3.0.0 - Flutter
>=3.10.0
Installation
Section titled “Installation”Add to your pubspec.yaml:
dependencies: uchara_sdk: ^1.0.1Then run:
flutter pub getOverview
Section titled “Overview”The SDK exposes a single headless VisitorSDK class. It wraps the
/v1/widget/* REST endpoints and the /ws/visitor WebSocket connection. Because
it is headless, it works on any platform Flutter supports (iOS, Android, web,
macOS, Linux, Windows) and never assumes a particular UI.
Configuration
Section titled “Configuration”Create a VisitorConfig with the API base URL and the public
widget token for your channel:
final config = VisitorConfig( apiUrl: 'https://api.uchara.com', widgetToken: 'wgt_your_token', identity: VisitorIdentity( externalId: 'user_123', name: 'Jane Doe', email: 'jane@example.com', phone: '+6281234567890', metadata: {'plan': 'pro'}, ), autoConnect: true, // open the realtime socket after init() autoReconnect: true, // reconnect after an unexpected drop maxReconnectAttempts: 10, // bounded exponential backoff tokenStore: MyTokenStore(), // optional durable persistence);VisitorConfig
Section titled “VisitorConfig”| Field | Type | Default | Description |
|---|---|---|---|
apiUrl | String | — | Base URL of the Uchara API, e.g. https://api.uchara.com. Must use http/https. |
widgetToken | String | — | Public widget token for the channel. |
identity | VisitorIdentity? | null | Identity metadata sent during session initialisation. |
autoConnect | bool | true | Open the realtime connection automatically after init(). |
timeout | Duration | 30s | Per-request HTTP timeout. |
tokenStore | TokenStore? | InMemoryTokenStore | Persistence for the visitor token. |
autoReconnect | bool | true | Auto-reconnect the WebSocket after an unexpected drop. |
maxReconnectAttempts | int | 10 | Maximum WebSocket reconnect attempts. |
initialReconnectDelay | Duration | 500ms | Delay before the first reconnect attempt. |
maxReconnectDelay | Duration | 30s | Upper bound for the reconnect delay. |
autoPresenceHeartbeat | bool | true | Automatically refresh visitor online presence while connected. |
presenceHeartbeatInterval | Duration | 20s | Presence refresh interval; the backend presence TTL is 35 seconds. |
autoRefreshSession | bool | true | Automatically renew the visitor JWT before expiry. |
sessionRefreshBeforeExpiry | Duration | 5m | How early to renew the visitor JWT. |
sessionRefreshInterval | Duration | 12h | Fallback refresh interval when token expiry cannot be decoded. |
autoPresenceHeartbeat | bool | true | Automatically refresh visitor online presence while connected. |
presenceHeartbeatInterval | Duration | 20s | Presence refresh interval; the backend presence TTL is 35 seconds. |
VisitorIdentity
Section titled “VisitorIdentity”Optional identity metadata used during session initialisation: externalId,
name, email, phone, and free-form metadata.
Session lifecycle
Section titled “Session lifecycle”init()
Section titled “init()”Exchanges the widget token for a visitor JWT and contact identity, persists the
token, and (when autoConnect is enabled) opens the realtime connection.
final VisitorSDK visitor = VisitorSDK(config);final VisitorSession session = await visitor.init();print(session.visitorToken);print(session.contactId);restoreSession()
Section titled “restoreSession()”Restores a previously persisted visitor session from the TokenStore so a
returning visitor does not need to re-authenticate. Returns true when a token
was restored, false when none was stored.
final bool restored = await visitor.restoreSession( conversationId: 'conv_abc123', // optionally restore the active conversation connect: true, // open the realtime connection (default));TokenStore
Section titled “TokenStore”The visitor token can be persisted across SDK instances. The default
InMemoryTokenStore keeps the token only for the lifetime
of the process. Provide your own TokenStore backed by e.g. shared_preferences
or flutter_secure_storage for durable persistence. The SDK also automatically
refreshes an initialized visitor session before the JWT expires; call
refreshSession() when an explicit refresh is needed:
abstract class TokenStore { Future<String?> read(); Future<void> write(String token); Future<void> clear();}Conversations
Section titled “Conversations”// Fetch the public widget configuration (channel name + config)final WidgetConfigResult cfg = await visitor.getConfig();
// Get the active conversation, or null when none existsfinal Conversation? active = await visitor.getActiveConversation();
// Start a new conversation (optionally with a first message)final Conversation conv = await visitor.startConversation( message: 'Hello!', metadata: {'source': 'ios'},);
// Close the conversationawait visitor.closeConversation(conv.id);When an active conversation is found or a new one is started, the SDK automatically reconnects the WebSocket to that conversation’s room so its events are received.
Messages
Section titled “Messages”Pagination
Section titled “Pagination”getMessages uses offset/limit pagination and returns a MessagePage with
messages, total, and perPage:
final MessagePage page = await visitor.getMessages( conv.id, limit: 50, offset: 0,);for (final Message m in page.messages) { print('${m.senderType}: ${m.content}');}Sending text
Section titled “Sending text”final Message msg = await visitor.sendMessage( conv.id, content: 'Thanks for your help!',);Note: Sending attachments inline via
sendMessageis not supported by the backend. The legacyattachmentIds/attachmentsparameters are deprecated and ignored. Upload files first withuploadFile— the backend creates the message carrying the attachment.
Uploading files
Section titled “Uploading files”final Message uploaded = await visitor.uploadFile( conv.id, filename: 'receipt.pdf', bytes: fileBytes, // List<int> contentType: 'application/pdf',);Downloading the transcript
Section titled “Downloading the transcript”downloadConversation returns the transcript as raw Uint8List rather than a
dart:io File, keeping the API platform-agnostic (including web):
final Uint8List transcript = await visitor.downloadConversation(conv.id);Realtime events
Section titled “Realtime events”The SDK exposes a broadcast stream of typed events via visitor.events. The
connection state is available via visitor.connectionState and
visitor.connectionStateStream.
visitor.events?.listen((WSEvent event) { switch (event) { case MessageNewEvent(:final message): // a new message arrived case MessageDeltaEvent(:final delta, :final done): // a streaming bot response chunk case TypingEvent(:final senderType, :final stop): // typing indicator started (stop == false) or stopped (stop == true) case PresenceEvent(:final online): // a member/contact presence update case ConversationResolvedEvent(:final conversationId): // the conversation was resolved case ConversationEvent(:final type, :final payload): // a generic conversation.* event case UnknownEvent(:final type): // a forward-compatible event the SDK does not recognise }});Sending realtime signals
Section titled “Sending realtime signals”visitor.sendTyping(conv.id); // start typing indicatorvisitor.stopTyping(conv.id); // stop typing indicatorvisitor.presenceHeartbeat(); // keep the visitor marked onlineConnection state
Section titled “Connection state”WsConnectionState is one of disconnected, connecting, connected,
reconnecting, or closed. The client keeps the connection alive with pings and
reconnects with bounded exponential backoff plus jitter.
Web-safe
Section titled “Web-safe”The SDK avoids dart:io in its public API. downloadConversation returns
Uint8List, and the WebSocket is abstracted behind a socket interface, so the
same code runs on web, mobile, and desktop.
Security caveat: token in the WebSocket query
Section titled “Security caveat: token in the WebSocket query”The realtime connection is opened to /ws/visitor?token=...&conv=.... The
visitor token is sent as a query parameter, which the backend requires. Query
strings can be logged by proxies and intermediaries, so treat the visitor token
as sensitive and avoid exposing it in places where it could be captured.
Error handling
Section titled “Error handling”The SDK throws typed errors so callers can react appropriately:
| Error | Meaning |
|---|---|
ServerEnvelopeException | Server returned a structured error envelope ({"ok":false,"error":{...}}). Exposes statusCode, code, requestId. |
ApiException | An HTTP error that did not carry a structured envelope. |
NetworkException | A transport-level failure (DNS, refused, reset). |
UcharaTimeoutException | A request exceeded its configured timeout. |
ProtocolException | A WebSocket protocol violation or malformed payload. |
All errors extend UcharaException, which carries a message and optional
details.
try { await visitor.sendMessage(conv.id, content: 'Hello');} on ServerEnvelopeException catch (e) { print('${e.statusCode} ${e.code}: ${e.message}');} on NetworkException catch (e) { print('Network error: ${e.message}');}Full example
Section titled “Full example”import 'package:flutter/material.dart';import 'package:uchara_sdk/uchara_sdk.dart';
class ChatPage extends StatefulWidget { const ChatPage({super.key}); @override State<ChatPage> createState() => _ChatPageState();}
class _ChatPageState extends State<ChatPage> { late final VisitorSDK visitor; final List<Message> messages = []; final TextEditingController _input = TextEditingController();
@override void initState() { super.initState(); visitor = VisitorSDK(VisitorConfig( apiUrl: 'https://api.uchara.com', widgetToken: 'wgt_your_token', )); _initChat(); }
Future<void> _initChat() async { await visitor.init();
visitor.events?.listen((event) { if (event is MessageNewEvent) { setState(() => messages.add(event.message)); } });
final conv = await visitor.getActiveConversation() ?? await visitor.startConversation(message: 'Hello!');
final page = await visitor.getMessages(conv.id, limit: 50); setState(() { messages.clear(); messages.addAll(page.messages); }); }
Future<void> _send() async { final convId = visitor.activeConversationId; if (convId == null || _input.text.isEmpty) return; final msg = await visitor.sendMessage(convId, content: _input.text); setState(() => messages.add(msg)); _input.clear(); }
@override void dispose() { visitor.dispose(); super.dispose(); }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Chat Support')), body: Column( children: [ Expanded( child: ListView.builder( itemCount: messages.length, itemBuilder: (_, i) => ListTile( title: Text(messages[i].content ?? ''), subtitle: Text(messages[i].senderType.name), ), ), ), Row( children: [ Expanded(child: TextField(controller: _input)), IconButton(icon: const Icon(Icons.send), onPressed: _send), ], ), ], ), ); }}