TWT Visitor SDK
Open the visitor chat page inside your app. After the host supplies an HTTPS URL, the SDK loads it in the platform WebView or WKWebView and handles navigation, permissions, download status reporting, and site-data cleanup.
If your app owns the WebView and does not use the SDK, see Embed the Visitor Chat in an App WebView.
Current version: 0.0.1
| Platform | Requirement | Integration |
|---|---|---|
| Android | minSdk 23 | Maven Central: io.github.twt-chat:visitor-sdk:0.0.1 |
| iOS | iOS 15+ | SPM: https://github.com/TWT-Chat/twt-visitor-ios.git (tag 0.0.1, product TwtVisitorSDK) |
| Flutter | Flutter ≥ 3.44, Dart ^3.12.2 | Git: https://github.com/TWT-Chat/visitor_flutter.git (ref 0.0.1) |
Integration
Android
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
dependencies {
implementation("io.github.twt-chat:visitor-sdk:0.0.1")
}
The SDK includes INTERNET, RECORD_AUDIO, and CAMERA as optional
permissions (required=false). On Android 9 and earlier, add
WRITE_EXTERNAL_STORAGE yourself when downloading to the public Downloads
directory.
iOS
In Xcode, choose File → Add Package Dependencies and enter:
https://github.com/TWT-Chat/twt-visitor-ios.git
Select Exact Version 0.0.1, choose the TwtVisitorSDK product, and set
the deployment target to iOS 15 or newer.
Declare these usage descriptions in Info.plist:
<key>NSMicrophoneUsageDescription</key>
<string>Use the microphone to send voice messages</string>
<key>NSCameraUsageDescription</key>
<string>Use the camera to take and send photos or videos</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>Save conversation images</string>
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
Keep App Transport Security at its default settings. Call public APIs on the main thread.
Flutter
dependencies:
visitor_flutter:
git:
url: https://github.com/TWT-Chat/visitor_flutter.git
ref: 0.0.1
flutter pub get
The Android project must configure mavenCentral() (see the Android section
above). If you use RepositoriesMode.PREFER_SETTINGS, also retain the Flutter
Engine repository https://storage.googleapis.com/download.flutter.io. iOS
does not need another SPM dependency; use the Info.plist declarations from
the iOS section.
Quick start
An integration needs only three things:
url(required): the URL configured in the console. It must use HTTPS.- Configuration fields: SDK options such as
isApp,language,theme,title,directChatId, andnewMessageSoundMode. All have defaults. query: business parameters for the visitor page. The SDK does not interpret keys; it only UTF-8-encodes them into the URL. Anonymous visitors can omit the map.
Do not build ?a=1&b=2 yourself. Never put a password, cookie, AppSecret, or
long-lived token in a URL.
With only url, the SDK produces:
<console-configured-URL>?lang=zh-cn&theme=system
The default configuration writes lang=zh-cn and theme=system. To show a
close button inside the app, set isApp = true.
Android
val handle = TwtVisitorSdk.start(
this,
VisitorConfig(url = "<console-configured-URL>"),
)
start opens VisitorActivity and returns a VisitorHandle. A non-Activity
context automatically receives FLAG_ACTIVITY_NEW_TASK. See Callbacks and events for download and new-message callbacks.
iOS
import TwtVisitorSDK
let config = VisitorConfiguration(
url: URL(string: "<console-configured-URL>")!
)
let handle = try TwtVisitorSDK.present(from: self, configuration: config, delegate: self)
The host dismisses the page. The handle is no longer valid after dismissal.
Flutter
Embed the page in the current layout:
import 'package:visitor_flutter/visitor_flutter.dart';
final config = VisitorConfiguration(
url: '<console-configured-URL>',
);
Expanded(child: VisitorView(configuration: config))
VisitorView must be placed in a parent with a definite size, such as
Expanded or SizedBox.
To open a separate native page:
final handle = await VisitorFlutter.present(configuration: config);
The separate page has no close API; it ends through the system back action or
the in-page close button.
Configuration
The fields are the same on all three platforms. The SDK writes selected fields
to the URL and overrides duplicate keys in query.
| Field | Default | In URL? | Description |
|---|---|---|---|
url | Empty (required) | Base URL | Console-configured URL; HTTPS only. HTTP, file:, javascript:, custom schemes, and user:pass credentials are rejected. |
query | Empty map | As provided | Business parameters; see the next section. Do not manually add ?a=1. |
isApp | false | Writes is_app=1 when true | In-app mode; displays the visitor-page close button. false does not write is_app=0. |
language | zh-cn | Always writes lang | en, zh-cn, zh-tw, ja, ko, de, fr, pt, ru, es, vi, th, id, ms, or tl. |
theme | system | Always writes theme | light, dark, or system. Runtime setTheme accepts only light or dark. |
title | null | No | Native title-bar text; an empty value uses the web page title. |
directChatId | null | Writes direct=1&chatid= when set | Opens a conversation directly and removes chat_id. |
newMessageSoundMode | web | Generally no | web: the page plays the sound; native: the host plays it from the new-message callback. Uses the JS bridge rather than the URL (Flutter Android present() additionally adds new_message_sound_mode). |
Write order is: query already present in url → config.query →
is_app/lang/theme/direct + chatid. Later values override earlier
ones.
Business query parameters
query is a transparent map. The SDK only encodes and appends it; it does not
interpret keys. Anonymous visitors can omit the map. Configure
is_app/lang/theme/direct/chatid through the typed fields instead of
putting them in query.
To bind a business user:
query = mapOf(
"sbs" to "user-123",
"sbs_mm" to signature,
"ranstr" to randomStr,
"name" to "Alex",
)
| Parameter | Required? | Description |
|---|---|---|
sbs | Required when binding a user | Unique user ID in your business system; the page uses it to identify a logged-in customer. |
sbs_mm | Required when sbs is present | Signature for sbs. |
ranstr | Required when a signature is used | Random string included in the signature. |
name | Optional | Visitor name, shown in the agent workspace. |
nickname | Optional | Visitor nickname. |
email | Optional | Visitor email address. |
phone | Optional | Visitor phone number. |
customer_remark | Optional | Customer remark submitted with login to the agent side. |
ext_fk_id | Optional | Existing visitor ID used to continue historical conversations. |
referer | Optional | Source page URL. referer_url is also accepted and is shown as a source link to agents. |
source_title | Optional | Source page title. |
Do not send visitor_id or source=android/ios/flutter; the visitor page
does not read them. Use isApp=true as the app indicator.
Session APIs
| Capability | Android | iOS | Flutter |
|---|---|---|---|
| Open a separate page | TwtVisitorSdk.start | TwtVisitorSDK.present | VisitorFlutter.present |
| Embed | — | makeEmbeddedController + register | VisitorView |
| Change theme | handle.setTheme | handle.setTheme | VisitorFlutter.setTheme / handle.setTheme |
| Report a download | handle.reportDownloadStatus | Same | VisitorFlutter.reportDownloadStatus |
| Close | handle.close() | handle.close() | System back for separate pages; host pop for embedded pages |
| Clear site data | TwtVisitorSdk.clearSiteData | TwtVisitorSDK.clearSiteData | VisitorFlutter.clearSiteData |
Download statuses are started, completed, failed, or cancelled.
requestId must match the request. Optional fields are path, progress,
sourceUrl, mimeType, bytes, errorCode, and message. On iOS, a
completed Photos asset uses photos://localIdentifier as its path.
Clear results are SUCCESS, INVALID_URL, UNSUPPORTED, IN_USE, or
FAILED (Flutter exposes lower camel-case values). IN_USE means a session
still holds the host.
To switch accounts: close every page → wait for destruction to complete → call
clearSiteData and wait for success → open the next account.
Callbacks and events
Callbacks run on the main thread. Deduplicate by requestId or messageId,
and mark unfinished downloads as failed when the page is destroyed.
Android — VisitorBridgeListener
onDownloadRequested: start the download, then callreportDownloadStatus.onNewMessage: a new message arrived.onBack(): back-button handling;truemeans the host handled it.onPermissionResult: permission result.
iOS — VisitorBridgeDelegate (weak reference)
visitorDownloadRequested(...)visitorNewMessage()(may be repeated)visitorBack() -> BoolvisitorPermissionResult(...)
Flutter — VisitorFlutter.events
Events are not buffered when there is no listener. Start listening when the page opens and cancel the subscription when it is disposed.
late final StreamSubscription<VisitorBridgeEvent> sub;
@override
void initState() {
super.initState();
sub = VisitorFlutter.events.listen((e) {
switch (e.type) {
case 'downloadRequested':
break;
case 'webReady':
case 'webLoadFailed':
case 'close':
case 'new_message':
break;
}
});
}
@override
void dispose() {
sub.cancel();
super.dispose();
}
Event names are downloadRequested, webReady, webLoadFailed, close,
new_message, and theme.
Handle back for an embedded page as follows:
PopScope(
canPop: false,
onPopInvokedWithResult: (_, __) async {
if (!await VisitorFlutter.handleBack()) {
if (context.mounted) Navigator.of(context).pop();
}
},
child: VisitorView(configuration: config),
)
handleBack() == true means the page consumed the back action, for example by
returning from a conversation detail to the list. Call
VisitorFlutter.reload() after an initial load failure when appropriate.
Runtime behavior
Navigation
Same-origin HTTPS stays inside the container. Cross-origin HTTPS opens in the system browser; other schemes are intercepted.
Storage
Cookies, LocalStorage, and IndexedDB persist by default. Third-party cookies and mixed content are disabled.
Permissions
Only same-origin HTTPS microphone and camera capture reaches a system prompt; cross-origin requests are rejected. Flutter embedded pages on Android allow the microphone but reject the camera. Use a separate page or the native SDK when camera capture is required.
File selection
Uses the system/WebKit file picker and supports single and multiple selection.
Downloads
WebView and in-page downloads are sent to the host. The host performs the download and reports the final status.
Back
The page gets the event first (about 300 ms, for closing previews and similar actions). If it does not consume it, the host is asked; only then is the container closed.
Frequently asked questions
Why does the page fail to open?
Check that the URL is HTTPS and that the query keys are valid. iOS also
validates directChatId.
Why cannot site data be cleared?
IN_USE means a session is still active. Close every related page, wait for
destruction to finish, and retry. iOS matches WebKit records by host, so sibling
subdomains in the same record may also be cleared, but data is never cleared
globally.
Why is an embedded Flutter page zero height?
Give VisitorView a definite height, for example by placing it inside
Expanded.
How do I open a specific conversation directly?
Set directChatId.
How do I bind an already logged-in user?
Pass sbs, sbs_mm, and ranstr in query. Do not pass visitor_id.
How do I show a close button in the app?
Set isApp = true.
How can the theme follow the system?
Initially use theme = system. Runtime changes can only switch to light or
dark.
What about weak networks, backgrounding, lock screens, and WebSockets?
Verify these on real devices. The SDK does not add another network layer.
Is a screenshot bridge or image clipboard supported?
There is no screenshot bridge. Image clipboard behavior is not guaranteed in all system WebViews.