Add chat to your app
Besides your website, you can talk to customers in your own app. Add a "Contact us" button that opens a full-page chat hosted by SealReply. There is no SDK to install, and it works with iOS, Android, Flutter and React Native. Messages, translation, the pre-chat form, business hours, the look and every other setting work as in the widget on your website, and agents still answer in the inbox.
1. Turn on the chat page and copy its link
First verify your domain, as in Install the chat. Sites that are not verified cannot turn it on.
Open Sites → your site → Install and, on the Use in your app card, click Turn on app chat.
Copy the chat page link:
texthttps://chat.sealreply.com/sk_…sk_…is your site key, the same asdata-site-keyin your website's embed code.
The switch is off by default. Until you turn it on, the link only says the chat is not available, so nobody who learns your site key can pass off a chat page as yours.
2. Open it in your app
The system's in-app browser works best: SFSafariViewController on iOS, Custom Tabs on Android, inAppBrowserView in Flutter. Images, files and voice messages need nothing more, and users get back to your app with Done or Back.
import SafariServices
let url = URL(string: "https://chat.sealreply.com/sk_…")!
present(SFSafariViewController(url: url), animated: true)// build.gradle: implementation("androidx.browser:browser:1.8.0")
import androidx.browser.customtabs.CustomTabsIntent
CustomTabsIntent.Builder().build()
.launchUrl(context, Uri.parse("https://chat.sealreply.com/sk_…"))// pubspec.yaml: url_launcher: ^6.3.0
import 'package:url_launcher/url_launcher.dart';
await launchUrl(
Uri.parse('https://chat.sealreply.com/sk_…'),
mode: LaunchMode.inAppBrowserView,
);// npm install react-native-webview
import { WebView } from "react-native-webview";
<WebView source={{ uri: "https://chat.sealreply.com/sk_…" }} />The chat page has no close button: users leave it with your app's own back or close button.
In a WebView
To show the chat inside your app's own screens (in a tab, say), use a WebView (WKWebView on iOS, WebView on Android, react-native-webview). Keep in mind:
- Turn on JavaScript and local storage. On Android set
settings.javaScriptEnabled = trueandsettings.domStorageEnabled = true. Without them the chat does not open, and users are a new visitor every time. - Images and files: an Android
WebViewneedsWebChromeClient.onShowFileChooser, or "+" does nothing. iOS andreact-native-webviewneed nothing. - Voice messages: your app needs the microphone permission (
NSMicrophoneUsageDescriptioninInfo.pliston iOS;RECORD_AUDIOon Android, granted inWebChromeClient.onPermissionRequest). Or turn voice messages off under Sites → Visitor features.
Apps built from web pages can simply go to the chat page link. The chat page cannot be put in an iframe, so no other website can wrap your chat in its own page.
3. Let agents recognize signed-in users
If your app has accounts, you can identify signed-in users as on your website: they show as verified, are the same person on every device, and see their earlier chats in the chat menu.
Turn it on under Sites → your site → Visitor identity verification and keep the secret on your server.
Your app asks your server for the current user's
identity_hash: HMAC-SHA256 of the user's ID with the secret, exactly as for your website (how).Add these after the
#of the chat link:texthttps://chat.sealreply.com/sk_…#external_id=42&identity_hash=<signature>&name=Ann%20Lee&email=ann%40example.comEvery value must be URL-encoded. The code below does it for you:
var parts = URLComponents()
parts.queryItems = [
URLQueryItem(name: "external_id", value: user.id),
URLQueryItem(name: "identity_hash", value: identityHash), // from your server
URLQueryItem(name: "name", value: user.name),
URLQueryItem(name: "email", value: user.email),
]
let url = URL(string: "https://chat.sealreply.com/sk_…#" + (parts.percentEncodedQuery ?? ""))!
present(SFSafariViewController(url: url), animated: true)val fragment = Uri.Builder()
.appendQueryParameter("external_id", user.id)
.appendQueryParameter("identity_hash", identityHash) // from your server
.appendQueryParameter("name", user.name)
.appendQueryParameter("email", user.email)
.build().encodedQuery
CustomTabsIntent.Builder().build()
.launchUrl(context, Uri.parse("https://chat.sealreply.com/sk_…#$fragment"))final fragment = Uri(queryParameters: {
'external_id': user.id,
'identity_hash': identityHash, // from your server
'name': user.name,
'email': user.email,
}).query;
await launchUrl(
Uri.parse('https://chat.sealreply.com/sk_…#$fragment'),
mode: LaunchMode.inAppBrowserView,
);const fragment = Object.entries({
external_id: user.id,
identity_hash: identityHash, // from your server
name: user.name,
email: user.email,
}).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join("&");
<WebView source={{ uri: `https://chat.sealreply.com/sk_…#${fragment}` }} />When a user signs out or another one signs in, open the chat page without these details (or with the new user's). The chat page then starts as a new visitor, and the previous user's chats do not show.
Keep the signature safe
identity_hash works like that user's chat password: anyone who has it can read their chats. Keep the secret on your server and never put it in your app.
A signed link never expires, and whoever has it can chat as that user: build it each time the chat opens, and never log or share it.
What follows # is never sent to any server (ours included), and the chat page removes it from the address as soon as it has read it (a reload in the same tab still knows the user). With Android Custom Tabs, though, Chrome may keep the whole link in that phone's browsing history. If that matters to you, use the WebView way below, which keeps the signature out of the link.
Keeping the signature out of the link (WebView)
In a WebView, set window.sealreplySettings before the page loads, as on a website, and leave external_id and identity_hash out of the link. If the link has them too, the link wins.
let settings = """
window.sealreplySettings = { identity: {
external_id: \(jsonString(user.id)),
identity_hash: \(jsonString(identityHash)),
name: \(jsonString(user.name))
} };
"""
let config = WKWebViewConfiguration()
config.userContentController.addUserScript(
WKUserScript(source: settings, injectionTime: .atDocumentStart, forMainFrameOnly: true)
)
let webView = WKWebView(frame: .zero, configuration: config)
webView.load(URLRequest(url: URL(string: "https://chat.sealreply.com/sk_…")!))// build.gradle: implementation("androidx.webkit:webkit:1.12.0")
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
import org.json.JSONObject
val identity = JSONObject()
.put("external_id", user.id)
.put("identity_hash", identityHash)
.put("name", user.name)
if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
WebViewCompat.addDocumentStartJavaScript(
webView,
"window.sealreplySettings = { identity: $identity };",
setOf("https://chat.sealreply.com"),
)
}
webView.loadUrl("https://chat.sealreply.com/sk_…")const settings = `window.sealreplySettings = { identity: ${JSON.stringify({
external_id: user.id,
identity_hash: identityHash,
name: user.name,
})} }; true;`;
<WebView
source={{ uri: "https://chat.sealreply.com/sk_…" }}
injectedJavaScriptBeforeContentLoaded={settings}
/>jsonString stands for turning a string into a JSON string literal (quoted and escaped), for example with JSONEncoder. Never paste a user's name into the code as it is.
4. Tell agents where the user came from
The widget on your website tells agents the page a visitor is on. An app has no address, so add after the #:
| Parameter | Meaning |
|---|---|
page_title | The screen's name, such as Order #1024 or Product: Blue jacket. Without it agents see App. |
page_url | The same page on your website (starting with http:// or https://), which agents can open. Leave it out if there is none. |
https://chat.sealreply.com/sk_…#page_title=Order%20%231024&page_url=https%3A%2F%2Fshop.example.com%2Forders%2F1024All parameters
They all go after the link's #, joined with &, every value URL-encoded. All are optional.
| Parameter | Meaning |
|---|---|
external_id | The user's ID in your system, up to 128 characters |
identity_hash | The signature of the user's ID; counts only with external_id |
name, email | Shown to agents; never replace what the visitor's profile already has |
attributes, attributes_hash | Member details: attributes is a JSON object (such as {"plan":"gold"}), signed as in Pass member details |
page_title, page_url | Where the user came from, as above |
The chat page follows the phone's language, and its dark mode follows the system too (if your site's look is set to follow the visitor's system).
Replies after the user has left your app
The chat page receives messages live only while it is open. To reach users who left before an agent replied:
- Email: when replies are slow, the chat page asks the user for their email (see When you're offline). Signed-in users who passed
emailare not asked again. - Your own push notifications: subscribe to the
message.createdwebhook. For a message from an agent (sender.typeisagent), callGET /v1/visitors/{visitor_id}with the event'sconversation.visitor_id(the API key needsvisitors:read) to getexternal_id, your user's ID, and notify them with your app's own push. When they tap it, open the chat page again and the reply is there.
Common questions
| What you see | What to do |
|---|---|
| "Chat is not available right now" | Check that Use in your app is on, that the site's domain is verified, that the site is not paused for being over your plan, and that the link's sk_… is complete. |
| Users are a new visitor every time | The WebView has no local storage (Android domStorageEnabled), or it is in private mode each time. |
| Signed-in users are not shown as verified | The signature is wrong. Check that you used this site's secret, signed exactly the external_id string, and URL-encoded the values in the link. |
| "+" does not open the photo picker | An Android WebView needs onShowFileChooser; or use Custom Tabs. |
Chats from your app count toward the site's monthly conversations like those from your website, and sit with your website's visitors in the inbox.