Skip to main content
Why a WebView? Botpress Cloud Webchat is a browser client (window.botpress). React Native does not run that API natively, so the supported pattern is to run the official Webchat inside a WebView and communicate with postMessage and injectJavaScript.The @botpress/webchat package targets web React. For native apps, use the WebView approach below.

Prerequisites

You will need:

1. Copy your Webchat embed code from the dashboard

Before you write any app code, grab the same embed snippet you would use on a website. In the Botpress dashboard:
  1. Open your bot’s Workspace and select the bot you want to embed.
  2. In the left sidebar, go to Webchat > Deploy Settings.
  3. Copy the Embed code:
Embed code from Dashboard

2. Lay out your project files

Create a src folder at your project root if you do not already have one. A typical layout:
PathPurpose
src/botpress/getBotpressWebchat.jsBuilds { html, baseUrl } for the WebView.
src/botpress/BpWidget.jsWebView component and event bridge (onMessage); blocks marketing site navigation on close.
src/botpress/BpWidget.d.ts(Optional, TypeScript only) Types for BpWidget.
src/config/botpressConfig.jsWebchat URLs and optional botId. Step 4 shows what goes inside export const botpressConfig = { ... }.
You can instead keep files flat under src/ and use relative imports. The examples below assume src/botpress/ and src/config/. Path alias (optional, Expo / TypeScript): In tsconfig.json, merge into compilerOptions:
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}
Then imports like import BpWidget from '@/botpress/BpWidget' resolve. If you skip aliases, use relative imports only.

3. Install react-native-webview

From your project root:
npx expo install react-native-webview
For bare React Native, follow react-native-webview linking if your setup requires it. Expo users can also see the Expo WebView docs.

4. Save your Studio script URLs in one config file

Create src/config/botpressConfig.js and export a single botpressConfig object. Paste the script URLs from Studio → Webchat → Embed (the same values from your embed snippet). The HTML helper and the widget only read botConfig: you pass it in, and you do not duplicate URLs inside getBotpressWebchat.js or BpWidget.js.
Sensitive values: botId and embed URLs identify your bot. For a public repository, do not commit real values, use placeholders in git or keep this file out of version control.
export const botpressConfig = {
  botId: "YOUR_BOT_ID",
  injectScriptUrl: "https://cdn.botpress.cloud/webchat/v3.6/inject.js",
  embedScriptUrl: "https://files.bpcontent.cloud/YOUR/PATH/YOUR-GENERATED.js",
  baseUrl: "https://cdn.botpress.cloud/",
};

5. Build the HTML page that loads Webchat

Add src/botpress/getBotpressWebchat.js. It builds a tiny HTML page (inject script, embed script, full height container) and returns { html, baseUrl } so you can pass it straight into <WebView source={{ baseUrl, html }} />.
getBotpressWebchat.js
/**
 * Builds the HTML page loaded by the WebView for Botpress Cloud webchat v3.
 * @param {Record<string, unknown>} botConfig Pass `botpressConfig` from src/config/botpressConfig.js
 * @returns {{ html: string, baseUrl: string }}
 */
const getBotpressWebchat = (botConfig) => {
  // Official inject script (Botpress loader); falls back to a known default if omitted.
  const injectUrl =
    botConfig.injectScriptUrl ||
    "https://cdn.botpress.cloud/webchat/v3.6/inject.js";
  // Studio-generated bundle URL (required). This wires your bot into the page.
  const embedUrl = botConfig.embedScriptUrl;
  if (!embedUrl || typeof embedUrl !== "string") {
    throw new Error(
      "botConfig.embedScriptUrl is required (paste the second script URL from Studio embed)",
    );
  }
  // Origin passed to WebView as `baseUrl` so relative URLs in the HTML resolve correctly.
  const baseUrl = botConfig.baseUrl || "https://cdn.botpress.cloud/";

  // Minimal HTML shell: viewport, full-height container, then inject + embed scripts (matches Studio order).
  const html = `<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <style>
      html, body { margin: 0; height: 100%; }
      body {
        display: flex;
        flex-direction: column;
        height: 100vh;
      }
      #bp-web-widget-container {
        height: 100%;
        width: 100%;
        flex: 1;
      }
    </style>
    <title>Chatbot</title>
  </head>
  <body>
    <script src="${injectUrl}"></script>
    <script src="${embedUrl}" defer></script>
  </body>
</html>`;

  // WebView expects this pair: `source={{ baseUrl, html }}`.
  return { baseUrl, html };
};

module.exports = getBotpressWebchat;

6. Show Webchat in a WebView and forward events to React Native

Create src/botpress/BpWidget.js. It renders a WebView that loads your HTML, then relays what happens inside the page to your React Native layer:
  • Puts the page from getBotpressWebchat(botConfig) into the WebView.
  • Injects a script that subscribes to window.botpress and sends each event to React Native with postMessage (you read them in onMessage as JSON strings).
  • Intercepts navigation to botpress.com or *.botpress.com when the user closes the widget and runs window.botpress.close() instead to close the webchat interface.
BpWidget.js
/**
 * WebView wrapper for Botpress webchat v3: window.botpress API, events to React Native via postMessage.
 */
import { WebView } from "react-native-webview";
import getBotpressWebchat from "./getBotpressWebchat";
import React, { useCallback, useRef } from "react";

const broadcastToReactNative = `
(function () {
  // Sends one event payload to React Native as a JSON string (read in onMessage).
  function post(ev, data) {
    try {
      window.ReactNativeWebView.postMessage(JSON.stringify({ event: ev, data: data }));
    } catch (e) {}
  }
  // Subscribes to window.botpress once it exists and mirrors events into React Native.
  function wire() {
    if (!window.botpress || typeof window.botpress.on !== "function") return false;
    window.botpress.on("webchat:initialized", function () {
      try { window.botpress.open(); } catch (e) {}
    });
    [
      "message",
      "webchat:initialized",
      "webchat:ready",
      "webchat:opened",
      "webchat:closed",
      "customEvent",
      "error",
      "conversation",
    ].forEach(function (ev) {
      window.botpress.on(ev, function (data) {
        post(ev, data);
      });
    });
    // Fallback open in case the widget did not auto-open after init.
    setTimeout(function () {
      try { window.botpress.open(); } catch (e) {}
    }, 400);
    return true;
  }
  // Poll until botpress is ready (or give up after ~12s) so inject order does not race.
  var n = 0;
  var id = setInterval(function () {
    if (wire() || ++n > 240) clearInterval(id);
  }, 50);
})();
true;
`;

const closeChatJs = `
try {
  // Programmatic close when we block a navigation attempt (through onShouldStartLoadWithRequest).
  if (window.botpress && typeof window.botpress.close === "function") {
    window.botpress.close();
  }
} catch (e) {}
true;
`;

// Returns true when a navigation URL is the public Botpress marketing site (close button behavior).
function isBotpressMarketingSiteUrl(url) {
  if (!url || url.startsWith("about:") || url.startsWith("blob:") || url.startsWith("data:")) {
    return false;
  }
  try {
    const h = new URL(url).hostname;
    return h === "botpress.com" || h.endsWith(".botpress.com");
  } catch {
    return false;
  }
}

export default function BpWidget(props) {
  const { botConfig, onMessage } = props;
  const webref = useRef(null);

  // Build the in-memory HTML page and origin for this WebView instance.
  const { html, baseUrl } = getBotpressWebchat(botConfig);

  // Intercept navigations: block opening botpress.com inside the frame and close chat instead.
  const onShouldStartLoadWithRequest = useCallback((request) => {
    const { url } = request;
    if (isBotpressMarketingSiteUrl(url)) {
      webref.current?.injectJavaScript(closeChatJs);
      return false;
    }
    return true;
  }, []);

  // Webchat needs JavaScript and DOM storage; originWhitelist allows loading scripts from Botpress CDNs.
  return (
    <WebView
      ref={webref}
      style={{ flex: 1 }}
      source={{
        baseUrl,
        html,
      }}
      onMessage={onMessage}
      injectedJavaScript={broadcastToReactNative}
      onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
      setSupportMultipleWindows={false}
      javaScriptEnabled
      domStorageEnabled
      originWhitelist={["*"]}
    />
  );
}
onMessage format: event.nativeEvent.data is a string. Parse with JSON.parse. Shape: { event: string, data: unknown }.

7. Optional: add TypeScript types for the widget

If you use TypeScript, add src/botpress/BpWidget.d.ts next to BpWidget.js so imports and props are typed:
import type { FunctionComponent } from "react";

/** Props for the WebView wrapper: Studio-derived config and optional React Native message handler. */
export interface BpWidgetProps {
  botConfig: Record<string, unknown>;
  onMessage?: (e: { nativeEvent: { data: string } }) => void;
}

/** Default export from BpWidget.js (JavaScript component with these props). */
declare const BpWidget: FunctionComponent<BpWidgetProps>;

export default BpWidget;

8. Render the Webchat widget on a screen

Import BpWidget and botpressConfig, then render it full screen or inside any View that uses flex: 1. Adjust import paths to your real paths (for example Expo Router app/index.tsx, a root App.js, or src/screens/…).
ChatScreen.tsx
import { StyleSheet, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";

import BpWidget from "@/botpress/BpWidget";
import { botpressConfig } from "@/config/botpressConfig";

/** Screen that fills the safe area with the Botpress WebView-powered widget. */
export default function ChatScreen() {
  return (
    <SafeAreaView style={styles.safeArea}>
      <View style={styles.main}>
        <BpWidget
          botConfig={botpressConfig}
          onMessage={(e) => {
            // Bridge: WebView posts JSON strings; parse to `{ event, data }` for logging or app logic.
            const raw = e.nativeEvent?.data;
            if (!raw) return;
            try {
              const msg = JSON.parse(raw);
              console.log(msg.event, msg.data);
            } catch {
              /* non-JSON */
            }
          }}
        />
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  safeArea: { flex: 1 },
  main: { flex: 1 },
});

9. Run your app

npm install
npx expo start
Open the iOS Simulator, Android emulator, or a physical device from there. Your bot should load inside the WebView when you run the app.

Troubleshooting

ProblemWhat to check
Blank WebViewembedScriptUrl in botpressConfig.js must be the files.bpcontent.cloud/... URL from Studio.
Error: embedScriptUrl is requiredSet embedScriptUrl in botpressConfig.
Close (X) opens the Botpress.com websiteKeep onShouldStartLoadWithRequest and closeChatJs in BpWidget.js as shown in that section.
Fonts look wrongTune appearance in Botpress Studio; avoid forcing global font-family in the HTML shell.
Android keyboard covers the composerIn app.json (Expo), under expo.android, set "softwareKeyboardLayoutMode": "resize". Rebuild the native Android app after changing this (not only a Metro refresh).

Next steps

Now that Webchat runs in your app, try styling it in Studio to match your product. For more control over the embed, see Embed Webchat and the Webchat interaction reference.
Last modified on March 31, 2026