Skip to main content

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

PlatformRequirementIntegration
AndroidminSdk 23Maven Central: io.github.twt-chat:visitor-sdk:0.0.1
iOSiOS 15+SPM: https://github.com/TWT-Chat/twt-visitor-ios.git (tag 0.0.1, product TwtVisitorSDK)
FlutterFlutter ≥ 3.44, Dart ^3.12.2Git: 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:

  1. url (required): the URL configured in the console. It must use HTTPS.
  2. Configuration fields: SDK options such as isApp, language, theme, title, directChatId, and newMessageSoundMode. All have defaults.
  3. 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.

FieldDefaultIn URL?Description
urlEmpty (required)Base URLConsole-configured URL; HTTPS only. HTTP, file:, javascript:, custom schemes, and user:pass credentials are rejected.
queryEmpty mapAs providedBusiness parameters; see the next section. Do not manually add ?a=1.
isAppfalseWrites is_app=1 when trueIn-app mode; displays the visitor-page close button. false does not write is_app=0.
languagezh-cnAlways writes langen, zh-cn, zh-tw, ja, ko, de, fr, pt, ru, es, vi, th, id, ms, or tl.
themesystemAlways writes themelight, dark, or system. Runtime setTheme accepts only light or dark.
titlenullNoNative title-bar text; an empty value uses the web page title.
directChatIdnullWrites direct=1&chatid= when setOpens a conversation directly and removes chat_id.
newMessageSoundModewebGenerally noweb: 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",
)
ParameterRequired?Description
sbsRequired when binding a userUnique user ID in your business system; the page uses it to identify a logged-in customer.
sbs_mmRequired when sbs is presentSignature for sbs.
ranstrRequired when a signature is usedRandom string included in the signature.
nameOptionalVisitor name, shown in the agent workspace.
nicknameOptionalVisitor nickname.
emailOptionalVisitor email address.
phoneOptionalVisitor phone number.
customer_remarkOptionalCustomer remark submitted with login to the agent side.
ext_fk_idOptionalExisting visitor ID used to continue historical conversations.
refererOptionalSource page URL. referer_url is also accepted and is shown as a source link to agents.
source_titleOptionalSource 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​

CapabilityAndroidiOSFlutter
Open a separate pageTwtVisitorSdk.startTwtVisitorSDK.presentVisitorFlutter.present
Embed—makeEmbeddedController + registerVisitorView
Change themehandle.setThemehandle.setThemeVisitorFlutter.setTheme / handle.setTheme
Report a downloadhandle.reportDownloadStatusSameVisitorFlutter.reportDownloadStatus
Closehandle.close()handle.close()System back for separate pages; host pop for embedded pages
Clear site dataTwtVisitorSdk.clearSiteDataTwtVisitorSDK.clearSiteDataVisitorFlutter.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 call reportDownloadStatus.
  • onNewMessage: a new message arrived.
  • onBack(): back-button handling; true means the host handled it.
  • onPermissionResult: permission result.

iOS — VisitorBridgeDelegate (weak reference)

  • visitorDownloadRequested(...)
  • visitorNewMessage() (may be repeated)
  • visitorBack() -> Bool
  • visitorPermissionResult(...)

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.