#Flutter SDK
A Dart package with no UI. It keeps the conversation — messages, typing,
connection — in a ChangeNotifier, and you draw it however your app looks.
Android, iOS and macOS, Flutter 3.22 or later; its only dependencies are
http, shared_preferences and package_info_plus.
#Setup
Create a publishable key that allows your app. A Flutter app has one origin
per platform: ios-app:// plus its bundle id (iOS and macOS), and
android-app:// plus its package name:
vatio keys create publishable --env live --label app \ --origin ios-app://com.acme.app --origin android-app://com.acme.appTo add them to an existing key instead, use vatio keys origins PREFIX URL...
or Settings → API keys in the inbox.
Add the package from pub.dev:
flutter pub add vatioAndroid release builds need the INTERNET permission in
android/app/src/main/AndroidManifest.xml (Flutter's template only grants it
to debug builds), and macOS needs com.apple.security.network.client in both
.entitlements files.
Then create a client with the vatpub_ key as token. It is meant to ship inside the
app; a vat_ developer token must never.
import 'package:vatio/vatio.dart';
final vatio = Vatio(workspace: 'acme', token: 'vatpub_REPLACE_ME');final chat = VatioChat(vatio);The SDK reads the bundle id or package name from the app itself, so the same code sends the right origin on each platform.
#Using it
chat.messages // List<VatioMessage>, oldest first: id, role, content, createdAt, isPendingchat.isTyping // the agent is writingchat.status // idle, connecting, connected, reconnecting, closed
await chat.send('What does Acme sell?');VatioChat notifies its listeners on every change, so a ListenableBuilder
(or Provider's and Riverpod's ChangeNotifierProvider) redraws as the reply
arrives. Creating a chat opens nothing: the conversation starts on the first
send, and the visitor's message is in messages at once, with isPending
set until the server confirms it. Call chat.resume() when the screen appears
to reopen the visitor's conversation, if they have one, and chat.dispose()
from the dispose of the widget or service that owns it.
ListenableBuilder( listenable: chat, builder: (context, _) => ListView( reverse: true, children: [ if (chat.isTyping) const TypingDots(), for (final message in chat.messages.reversed) Bubble(message), ], ),)The package README has the complete example: a text bar that switches to an Assistant tab and sends. An AI agent in your Flutter app walks through a whole app, from the key to a signed-in user's private tools.
| Method | Purpose |
|---|---|
vatio.config() | Agent name, avatar, accent color, greeting and suggestions |
vatio.conversations(visitorToken:) | This visitor's conversations on this device, newest first |
chat.send(text, attachments:) | Send a message, optionally with files; starts the conversation on first call |
chat.resume() | Reopen the stored conversation; never starts a new one |
chat.open(conversation) | Switch to a conversation from conversations() |
chat.startOver() | Forget the stored conversation; server history is kept |
chat.rate(rating, comment:) | Answer the feedback moment: good, neutral, bad |
chat.dismissFeedback() | Decline the feedback moment |
chat.close() | Disconnect; resume() or send reconnects |
chat.dispose() | Disconnect for good |
send throws a VatioException (code, message, status) and removes the
pending message, so put the text back in your field. Failures outside a
send land in chat.lastError.
#Files and voice notes
Attach up to four VatioUploads — images, PDFs, text files or audio, up to
8 MB each, as on the web. With files,
the text may be empty:
await chat.send('', attachments: [await VatioUpload.fromPath(photo.path)]); // image_picker, file_pickerawait chat.send('', attachments: [await VatioUpload.fromPath(recordingPath)]); // a recorder's .m4aOnce the server answers, the pending message is replaced by the stored one,
and each VatioAttachment has a url. A voice note is transcribed in the
background: the message reads "Audio" at first, and its content becomes the
transcript a few seconds later, in place; the agent answers after that. When
isMediaLabel is true, content is only a stand-in ("Image") — draw the
attachment instead. The example app has a composer with a photo picker.
#Visitor feedback
When a request looks resolved, Vatio offers a feedback moment, at most once
per conversation (see the widget guide).
chat.feedback holds it; show your own prompt while isOpen:
final feedback = chat.feedback;if (feedback != null && feedback.isOpen) { return RatingCard( question: feedback.question ?? 'How did ${feedback.agentName} do?', onRate: (rating, comment) => chat.rate(rating, comment: comment), );}question is how the agent asks, written for that conversation, or null
when there is none. It goes back to null when the visitor writes again
without answering.
#Signed-in users
Pass a visitor token your backend signed to say who the user is, as described in Sessions and channels:
final chat = VatioChat(vatio, visitorToken: signedToken);A different token subject is a different person with their own conversation, so signing out of your app signs out of the chat: dispose it and create a new one. An anonymous conversation is kept when the user signs in.
#Delivery and storage
Replies arrive over a WebSocket. When it drops, the SDK reconnects, polls in
the meantime, and fetches what it missed; messages are deduplicated by id.
replyStyle (VatioReplyStyle.stream by default, paced, instant) works as
on the web.
The conversation and the visitor id are stored with shared_preferences, per
workspace and signed-in user. Chat credentials last 12 hours. Rate limits and
message length are the same as on the web. On the web, use the
widget or the browser SDK instead: this package
runs on Android, iOS and macOS.