Embeding in WebView (Android and iOS)

Overview

This guide explains how to embed the webchat in a WebView component of a native mobile application using the Native Host API.

The Native Host API replaces the PostMessage handshake. Instead of exchanging AWAITING_CONFIG / SET_CONFIG / START_CONVERSATION messages, the host application:

  • creates the global window.webchat object with the configuration before the webchat page loads,
  • optionally adds an onEvent callback to it to receive webchat events,
  • optionally calls methods that the webchat adds to the same object to control it at runtime.

The webchat is loaded directly (top-level) in the WebView — there is no iframe and no chat-ui-launcher script involved. The host application does not need to know any event names or message formats to start a conversation.

Available from v266.

The PostMessage mechanism remains fully supported. Existing integrations continue to work without changes, and both mechanisms can be used side by side.

How it works

The contract between the host application and the webchat is a single global object, window.webchat, in the WebView's JavaScript context:

MemberProvided byPurpose
window.webchat.confighost, before the page loadsConfiguration object, read once when the webchat starts
window.webchat.onEvent(type, payload)host, optionalCallback receiving webchat events
window.webchat.updateConfig(), sendEvent(), ...webchat, after it mountsMethods for runtime control

The host creates window.webchat with config (and optionally onEvent) before the webchat's JavaScript executes. The webchat then adds its methods to the same object — it never replaces it. How the host defines the object before load is platform-specific (see the examples below).

Reserved names: updateConfig, startConversation, sendEvent, destroy. Do not define them in your object; the webchat overwrites them.

URL to load

Load the following URL in the WebView:

https://webchat.app.chatbots.sentione.com/fullscreen/CHANNEL_ID

Replace CHANNEL_ID with the ID of the channel configured in the admin panel. The channel configuration (appearance, handover, translations) is applied automatically and window.webchat.config is merged on top of it.

Providing the configuration: window.webchat.config

The configuration object has the same shape as the initWebChat configuration used by the chat-ui-launcher script (see Configuration). A minimal configuration:

{
  "session": {
    "id": "unique-session-identifier"
  },
  "author": "John Doe",
  "actionButtons": {
    "conversationWindow": {
      "minimize": false,
      "resetAndMinimize": false
    }
  }
}
  • session.id — see Important Note on Session Management.
  • author — name of the user, shown in the conversation and passed to the bot.
  • actionButtons.conversationWindow — in a WebView there is usually nothing to minimize to, so both buttons are typically disabled.

When window.webchat.config is present, the webchat applies it, starts the conversation automatically and does not wait for any PostMessage. The WEBCHAT_READY event is still emitted for compatibility.

The value is read once, when the webchat starts. To change the configuration later use window.webchat.updateConfig(...).

Controlling the webchat at runtime: window.webchat

After the webchat has started, the following methods are added to window.webchat. Call them from native code with evaluateJavascript.

MethodDescription
updateConfig(config)Merges config into the current configuration (e.g. change author, language, appearance).
startConversation()Starts a new conversation. Not needed on load — the conversation starts automatically when config is provided. Use it to start again after the conversation was closed (CLOSE_CONVERSATION).
sendEvent({ label, extraData })Sends an event message to the bot, e.g. to trigger a specific flow. Starts the conversation first if there is none.
destroy()Closes the conversation session and emits DESTROY_DONE. The page itself stays interactive — typing starts a brand-new conversation — so the host should hide or close the WebView on DESTROY_DONE.

The methods exist only after the webchat has mounted. Use optional chaining (window.webchat?.updateConfig?.(...)) so that a call made before the page has finished loading is a no-op instead of an error.

Receiving events: window.webchat.onEvent

If the host defines window.webchat.onEvent(type, payload), the webchat calls it for every event it emits. type is the event name, payload is always a JSON string — JSON.parse(payload) returns the event data, or null when the event has no payload.

EventPayloadDescription
CHANNEL_CONFIGchannel configuration objectSent first, when the channel configuration has been loaded (channel URLs only).
WEBCHAT_READY—Configuration applied. Emitted on load and again after every updateConfig() / SET_CONFIG.
CLIENT_MESSAGE_SENTmessage textThe user sent a message. For event messages sent with sendEvent() the payload is null (no text).
MESSAGE_RECEIVEDmessage objectA message from the bot or a live agent arrived. Bot messages contain responses[].text; agent messages contain text.
CONVERSATION_END—The conversation is finished.
CLOSE_CONVERSATION_WINDOW—The user clicked the close (X) icon.
MINIMIZE_CONVERSATION_WINDOW—The user clicked the minimize icon.
DESTROY_DONE{ requestSuccess, requestStatus, requestStatusText }The webchat has been destroyed; the payload describes the result of the session-closing request.
ANNOUNCEMENT_BANNER_LINK_CLICKED{ href, content, announcementBannerConfig }The user clicked a link in the announcement banner. Emitted only when announcementBanner.markdownLinkBehavior is "callback"; with "new-tab" the link simply opens.
CAROUSEL_BUTTON_CLICKED{ button }The user clicked a carousel button of type notify_host. Buttons that open a URL emit nothing.

Defining onEvent is optional. Without it the webchat works normally; the events are simply not delivered anywhere.

Example Android implementation

The example uses WebViewCompat.addDocumentStartJavaScript from androidx.webkit to define window.webchat before the page scripts run. Events are forwarded to a native object registered with addJavascriptInterface under a different name (webchatNative) — the interface must not be registered as webchat, because that would replace the shared object with the native proxy.

Add the dependency:

implementation("androidx.webkit:webkit:1.12.1")

Make sure the manifest contains <uses-permission android:name="android.permission.INTERNET" /> and the activity has android:windowSoftInputMode="adjustResize" so the keyboard does not cover the message input.

package com.example.myapplication

import android.os.Bundle
import android.util.Log
import android.webkit.ConsoleMessage
import android.webkit.JavascriptInterface
import android.webkit.WebChromeClient
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
import com.example.myapplication.databinding.ActivityMainBinding
import org.json.JSONObject
import java.util.UUID

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.webView.settings.apply {
            javaScriptEnabled = true
            domStorageEnabled = true
            mediaPlaybackRequiresUserGesture = false
        }

        binding.webView.webChromeClient = object : WebChromeClient() {
            override fun onConsoleMessage(message: ConsoleMessage): Boolean {
                Log.d("WebView", "${message.message()} (${message.sourceId()}:${message.lineNumber()})")
                return true
            }
        }

        if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
            WebViewCompat.addDocumentStartJavaScript(
                binding.webView,
                """
                window.webchat = {
                  config: ${buildHostConfig()},
                  onEvent: (type, payload) => webchatNative.onEvent(type, payload)
                };
                """.trimIndent(),
                setOf("*")
            )
        } else {
            Log.e("WebView", "DOCUMENT_START_SCRIPT not supported by this WebView")
        }

        binding.webView.addJavascriptInterface(WebchatNativeBridge(), "webchatNative")
        binding.webView.loadUrl(WEBCHAT_URL)
    }

    private fun buildHostConfig(): String = JSONObject()
        .put("session", JSONObject().put("id", sessionId()))
        .put("author", "John Doe")
        .put(
            "actionButtons",
            JSONObject().put(
                "conversationWindow",
                JSONObject().put("minimize", false).put("resetAndMinimize", false)
            )
        )
        .toString()

    private fun sessionId(): String =
        getPreferences(MODE_PRIVATE).getString("webchat_session", null)
            ?: UUID.randomUUID().toString().also {
                getPreferences(MODE_PRIVATE).edit().putString("webchat_session", it).apply()
            }

    inner class WebchatNativeBridge {
        @JavascriptInterface
        fun onEvent(type: String, payload: String) {
            Log.d("WebView", "event $type: $payload")
            when (type) {
                "MESSAGE_RECEIVED" -> onMessageReceived(JSONObject(payload))
                "CONVERSATION_END" -> runOnUiThread {
                    Toast.makeText(this@MainActivity, "Conversation ended", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }

    private fun onMessageReceived(message: JSONObject) {
        val text = message.optJSONArray("responses")?.optJSONObject(0)?.optString("text")
            ?: message.optString("text")
        if (text.isNullOrBlank()) return

        runOnUiThread {
            Toast.makeText(this, "New message: $text", Toast.LENGTH_SHORT).show()
        }
    }

    private fun updateWebchatConfig(config: JSONObject) {
        binding.webView.evaluateJavascript("window.webchat?.updateConfig?.($config)") { result ->
            Log.d("WebView", "updateConfig -> $result")
        }
    }

    private fun sendWebchatEvent(label: String, extraData: JSONObject = JSONObject()) {
        val payload = JSONObject().put("label", label).put("extraData", extraData)
        binding.webView.evaluateJavascript("window.webchat?.sendEvent?.($payload)", null)
    }

    companion object {
        private const val WEBCHAT_URL = "https://webchat.app.chatbots.sentione.com/fullscreen/CHANNEL_ID"
    }
}

Notes:

  • onEvent is called on a background thread — use runOnUiThread before touching the UI.
  • Build the configuration with JSONObject instead of concatenating strings. The result is inserted into JavaScript as a literal, so values containing quotes or special characters are handled correctly.
  • addDocumentStartJavaScript is not available on very old system WebViews. Check WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT) and fall back to the PostMessage integration when it returns false.

Configure channel

Because the webchat is loaded directly in the WebView (not inside an iframe on a web page), it reports its own origin as the embedding domain. In the allowed domains field of the channel configuration add the WebChat URL used in the WebView, for example:

https://webchat.app.chatbots.sentione.com

Alternatively, the application can send a Referer header with its own domain when loading the URL (e.g. loadUrl(url, mapOf("Referer" to "https://your-app.example.com"))). In that case add that domain to the allowed domains instead.

Without a matching entry every request from the webchat is rejected with Domain ... is not allowed in this subscription.

Important Note on Session Management

The session.id field is crucial for proper webchat functionality.

  • The session ID should be generated on the host side (your application).
  • It is the host's responsibility to store and manage this session ID.
  • The host should keep the session ID up to date and reset it when necessary (e.g. starting a new conversation, user logout).

Proper session management ensures conversation continuity and user experience consistency. A stable session ID across application restarts restores the conversation history.

Migrating from the PostMessage integration

PostMessage integrationNative Host API
Wait for AWAITING_CONFIG, send SET_CONFIGDefine window.webchat.config before load
Wait for WEBCHAT_READY, send START_CONVERSATIONNot needed on load — the conversation starts automatically. window.webchat.startConversation() restarts it later (e.g. after CLOSE_CONVERSATION)
SET_CONFIG at runtimewindow.webchat.updateConfig(...)
TRIGGER_BOTwindow.webchat.sendEvent(...)
DESTROYwindow.webchat.destroy()
window.addEventListener('message', ...)window.webchat.onEvent(type, payload)

Both mechanisms can coexist during migration — a SET_CONFIG message received after window.webchat.config is applied as a configuration update.

Troubleshooting

  • The webchat shows the header but no message input: window.webchat.config was not defined before the page scripts ran. Make sure the script is injected at document start (addDocumentStartJavaScript), not after page load.
  • Requests fail with Domain ... is not allowed in this subscription: add the webchat origin (or the Referer domain) to the channel's allowed domains.
  • window.webchat.updateConfig is undefined: the webchat has not mounted yet. Call the methods after the page has loaded and use optional chaining.
  • Events do not arrive in native code: check that onEvent is defined on window.webchat at document start and that the native interface / message handler (webchatNative) is registered before loadUrl / load.
  • Nothing works and the WebView is blank: check that JavaScript is enabled and, on Android, that the URL is reachable (cleartext HTTP is blocked by default — use HTTPS).

Using PostMessage instead

The webchat also accepts commands and emits events through the standard window.postMessage API. This mechanism keeps working unchanged and can be used instead of, or alongside, the Native Host API. Because the webchat is loaded top-level in the WebView, window.parent is the webchat window itself, so messages posted from native code are delivered to the webchat directly.

Messages in both directions have the same shape:

{ "type": "EVENT_TYPE", "payload": { } }

Commands accepted by the webchat

TypePayloadDescription
SET_CONFIGconfiguration objectInitializes or updates the configuration. Send it in response to AWAITING_CONFIG.
START_CONVERSATION—Starts the conversation. Send it after WEBCHAT_READY.
TRIGGER_BOT{ label, extraData }Sends an event message to the bot.
CLOSE_CONVERSATION—Closes and resets the current conversation.
DESTROY—Destroys the webchat and closes the session.

Events emitted by the webchat — the same list as for onEvent above, plus AWAITING_CONFIG, sent when the webchat is mounted and waiting for SET_CONFIG. AWAITING_CONFIG is not emitted when window.webchat.config is present.

Sending a command from native code:

val message = JSONObject()
    .put("type", "SET_CONFIG")
    .put("payload", JSONObject().put("session", JSONObject().put("id", sessionId())))
webView.evaluateJavascript("window.postMessage($message, '*')", null)

Receiving events requires a message listener in the page that forwards them to native code, for example injected with addDocumentStartJavaScript:

window.addEventListener("message", (event) => {
  if (event.data && event.data.type) {
    webchatNative.onEvent(event.data.type, JSON.stringify(event.data.payload ?? null));
  }
});

The handshake order matters: wait for AWAITING_CONFIG before sending SET_CONFIG, and for WEBCHAT_READY before START_CONVERSATION. The Native Host API is the recommended option for new integrations because it removes this handshake and delivers events straight to native code.


Did this page help you?