Hybrid App integration

A Hybrid App uses both native URL requests and in-app WebViews to talk to your servers. HUMAN must treat those as one visitor: the native SDK VID and the WebView VID must be the same.

How do I know if my app is a hybrid app

Your app is a hybrid app if both of the following are true:

  1. It loads your website (or a HUMAN-protected page on it) in an in-app WebView.
  2. That site is protected by HUMAN (Bot Defender or Account Defender).

You do not need to call the app “hybrid” in product language. These topologies all count:

  • Login, signup, account, billing, or help pages shown in a WebView.
  • OAuth or SSO pages on a login sub-domain of the same registrable root (for example login.example.com when the root is example.com).
  • A Cordova (or similar) app whose UI is a whole-app WebView of a protected site.
  • Cross-platform WebViews: react-native-webview, Expo WebView, Flutter webview_flutter, Ionic/Capacitor.

A WebView that only loads an unprotected static page (for example, a privacy policy that is not behind HUMAN) does not need hybrid configuration. However, if that same WebView later loads a protected login or account flow, treat it as hybrid.

Which WebViews are supported

Use WKWebView or Android WebView with setupWebView (or iOS automaticSetup). Hybrid is also supported for wrapper-hosted WebViews when you configure them as described in the WebView container support matrix.

SFSafariViewController and Chrome Custom Tabs need a special integration. Contact HUMAN; do not assume default hybrid setup will sync identity. Those containers own a cookie jar the app cannot write with the default Hybrid App path.

Domain configuration

Set hybridAppPolicy.webRootDomains to the bare registrable root of the site in the WebView.

Canonical form: example.com

  • No leading dot is preferred and should be used whenever possible. However, a leading dot is accepted and equivalent, which means .example.com is stored and matched the same as example.com. If your older samples use .example.com, there’s no need to correct them.
  • No scheme (https://)
  • No www
  • No path or port

This value is used to decide which WebView hosts get hybrid support and as the Domain when the SDK sets _pxmd and _pxwvm. webRootDomains = ["example.com"] is sufficient for the apex and every sub-domain of that root.

How matching works

A WebView host matches a configured domain when the host equals that value, or when the host ends with a dot plus that value. That is not a free-text suffix test: notexample.com does not match example.com.

WebView hostwebRootDomains = ["example.com"]
example.comMatch
www.example.comMatch
api.example.comMatch
login.example.comMatch
notexample.comNo match

Most common misconfiguration

Listing a sub-domain (api.example.com, login.example.com) instead of the registrable root is the most common failure.

It fails silently. Instrumentation still matches that host, so the integration looks healthy, but cookies are scoped to the sub-domain. Other hosts on the same site never share VID with native. Configured sub-domains are kept as-is; they are not reduced to the root.

A leading www. is not this failure: www.example.com is stripped to example.com (the same as a leading dot). Prefer the bare root example.com in new code.

Do not copy the dotted cookie name from browser DevTools into webRootDomains as a requirement. DevTools often shows .example.com; configure example.com.

Remote domain configuration

HUMAN can push hybrid root domains for your App ID from the Portal. The SDK unions those domains with the list in policy. A domain correction does not require an app release.

Still set webRootDomains in the app when you can. Contact HUMAN if you need a server-side domain update.

A real Set-Cookie: Domain=example.com header is not host-only: it is readable on the apex and on sub-domains.

Some test-harness APIs that take a bare domain string — for example Playwright addCookies({ domain: 'example.com' }) — can create a host-only cookie instead. That can make a lab test pass (or fail) in a way production never would. Treat the HTTP Set-Cookie header as the source of truth, not the harness helper.

Multiple App IDs and registrable domains

If the WebView loads a different registrable domain (not a sub-domain of the first root), list that root under the App ID that protects it.

val policy = HSPolicy().apply {
hybridAppPolicy.setWebRootDomains(setOf("example.com"), "PX123")
hybridAppPolicy.setWebRootDomains(setOf("other-shop.com"), "PX456")
}
HumanSecurity.start(this, listOf("PX123", "PX456"), policy)

OAuth on login.example.com does not need a second entry if example.com is already configured.

Enable Hybrid App support

Integrate the native SDK first:

  1. Easiest Implementation
  2. Basic Implementation
  3. Advanced Functionality

Pass the WebView instance to the SDK:

  • iOS: HumanSecurity.setupWebView(webView:navigationDelegate:)
  • Android: HumanSecurity.setupWebView(webView:webViewClient:)

Important: Do not set the navigationDelegate (iOS) or webViewClient (Android) after setupWebView.

On iOS, set HSHybridAppPolicy.automaticSetup to true to detect and set up WKWebView instances without calling setupWebView for each one.

Example implementation

Android

Kotlin:

import android.app.Application
import com.humansecurity.mobile_sdk.HumanSecurity
import com.humansecurity.mobile_sdk.main.policy.HSPolicy
class MainApplication : Application() {
override fun onCreate() {
super.onCreate()
startHumanSDK()
}
private fun startHumanSDK() {
try {
val policy = HSPolicy().apply {
// Configure the policy...
automaticInterceptorPolicy.interceptorType = HSAutomaticInterceptorType.INTERCEPT_WITH_DELAYED_RESPONSE // or INTERCEPT_AND_RETRY_REQUEST
hybridAppPolicy.setWebRootDomains(setOf("example.com"), "<APP_ID>")
}
HumanSecurity.start(this, "<APP_ID>", policy)
} catch (exception: Exception) {
println("Exception: ${exception.message}")
}
}
}

Java:

import android.app.Application;
import android.util.Log;
import java.util.HashSet;
import com.humansecurity.mobile_sdk.HumanSecurity;
import com.humansecurity.mobile_sdk.main.policy.HSPolicy;
import com.humansecurity.mobile_sdk.main.policy.HSAutomaticInterceptorType;
public class MainApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
startHumanSDK();
}
void startHumanSDK() {
try {
HSPolicy policy = new HSPolicy();
// Configure the policy...
policy.getAutomaticInterceptorPolicy().setInterceptorType(HSAutomaticInterceptorType.INTERCEPT_WITH_DELAYED_RESPONSE); // or INTERCEPT_AND_RETRY_REQUEST
HashSet<String> domains = new HashSet<>();
domains.add("example.com");
policy.getHybridAppPolicy().setWebRootDomains(domains, "<APP_ID>");
HumanSecurity.INSTANCE.start(this, "<APP_ID>", policy);
} catch (Exception exception) {
Log.e("MainApplication", "Exception: " + exception.getMessage());
}
}
}

Kotlin (Activity):

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.webkit.WebView
import com.humansecurity.mobile_sdk.HumanSecurity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val webView = findViewById<WebView>(R.id.web_view)
val webViewClient = MyWebViewClient()
HumanSecurity.setupWebView(webView, webViewClient)
// Load your website in the web view...
}
}

Java (Activity):

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.humansecurity.mobile_sdk.HumanSecurity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
WebView webView = findViewById(R.id.web_view);
MyWebViewClient webViewClient = new MyWebViewClient();
HumanSecurity.INSTANCE.setupWebView(webView, webViewClient);
// Load your website in the web view...
}
}

iOS

Swift:

import UIKit
import HUMAN
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
startHumanSDK()
return true
}
func startHumanSDK() {
do {
let policy = HSPolicy()
// Configure the policy...
policy.hybridAppPolicy.set(webRootDomains: ["example.com"], forAppId: "<APP_ID>")
HSAutomaticInterceptorPolicy.urlSessionRequestTimeout = 10 // Set the timeout you would like for your requests.
try HumanSecurity.start(appId: "<APP_ID>", policy: policy)
} catch {
print("Error: \(error)")
}
}
}

Objective-C:

@import HUMAN;
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self startHumanSDK];
return YES;
}
- (void)startHumanSDK {
HSPolicy *policy = [[HSPolicy alloc] init];
// Configure the policy...
[policy.hybridAppPolicy setWithWebRootDomains:[NSSet setWithObject:@"example.com"] forAppId:@"<APP_ID>"];
HSAutomaticInterceptorPolicy.urlSessionRequestTimeout = 10; // Set the timeout you would like for your requests.
NSError *error = nil;
[HumanSecurity startWithAppId:@"<APP_ID>" policy:policy error:&error];
if (error != nil) {
NSLog(@"Error: %@", error);
}
}
@end

Explanation of the code

  1. Web root domains: Set the registrable root (example.com) in webRootDomains. Prefer the bare form; a leading www. is stripped to that root. Do not use https://….
  2. Start the SDK as early as possible on the main thread so URL requests include HUMAN headers.
  3. Set up WebViews:
    • Android: HumanSecurity.setupWebView(webView:webViewClient:). Do not set the WebViewClient directly after this.
    • iOS: HumanSecurity.setupWebView(webView:navigationDelegate:). Do not set the navigationDelegate directly after this.

Notes:

  • iOS automatic setup: HSHybridAppPolicy.automaticSetup = true sets up WKWebView instances without calling setupWebView for each one.
  • Multiple App IDs: Use HumanSecurity.start(appIds:policy:) and pass the App ID that owns each root, as in the example above.

What a working integration looks like

The native VID and the WebView VID must be identical.

Confirm the following:

SideVID and where to read
NativeRead X-PX-VID on a request your app sends to a protected host (from SDK headers)
WebViewRead _pxvid cookie (and the matching Sensor session) on the loaded page

If your sync is successful, you should see the following cookies in an instrumented WebView:

NameWriterRole
_pxmdMobile SDKPayload for the Sensor (cookies channel only). Events and session-storage modes may show only _pxwvm plus Sensor cookies, so missing _pxmd in those modes is not a failed sync.
_pxwvmMobile SDKMarks the page as an in-app WebView
_pxvidSensorVisitor ID adopted from the SDK

If those VIDs differ after the first navigation, hybrid is not working. If that’s the case, refer to Troubleshooting.

Verification

Run the Doctor App Web view test before production. Treat a passing hybrid/WebView test as a release gate.

  1. Enable the Doctor App (HSDoctorAppPolicy.enabled = true). See How to verify the SDK integration in your app for more information.
  2. Choose the Web view framework test and exercise the WebView that loads your protected site.
  3. On the summary screen, confirm the test passed.
  4. Export the result JSON and keep it. That export is the standard attachment for HUMAN Support.

Disable the Doctor App (enabled = false) before you ship.

_pxhd in hybrid apps

_pxhd is an HTTP cookie that carries an encrypted VID and socket IP. On Enforcer traffic, it is consulted before _pxvid.

Inside an instrumented WebView, the SDK and Sensor manage _pxhd so the SDK’s VID wins. You may see _pxhd disappear in that WebView; that is expected.

If native and WebView VIDs diverge on a domain that is also a desktop property, _pxhd is the first thing support checks. Contact HUMAN; this is not fixed by changing webRootDomains alone.

Cookies and storage you must not delete

If the app clears cookies or WebView storage on logout, use Mobile SDK 5.3.0 or later. That release restores missing _pxmd / _pxwvm after a wipe (cookies channel; other channels restore _pxwvm as appropriate). Older SDKs stay broken until the next full handshake.

Prevention still matters, especially on Android native and wrapper-hosted WebViews where hybrid often depends on cookies. If you write deletion logic, skip HUMAN keys. Do not delete every cookie in the WebView store.

Cookies (skip these): _pxmd, _pxwvm, _pxvid, _pxmvid, _pxhd, _pxda, _px_mobile_data, _px / _px2 / _px3, _pxde, _pxff_*, _pxttld, pxcts, __pxvid

Local storage (skip these): _advanced_features, fsch, pxcts, px-ff, px_hvd, and Code Defender keys px_22j9f8hlau2f5, px_33df3rmnerrf5 if you use that product.

Session storage (skip these): _pr_c, px_c_p_, px_fp, px_nfsp, pxsid, pxtiming, and Code Defender px_11a381f6 if applicable.

Full descriptions: Use of cookies and web storage.

Troubleshooting

First check: webRootDomains is the bare root (example.com), not a sub-domain. Confirm matching with the table above.

Then check the following:

  1. The WebView is a supported container
  2. setupWebView / automaticSetup / supportExternalWebViews matches how the WebView is created
  3. Contact HUMAN if _pxhd is on a domain shared with desktop.

Check the VID split (native vs. _pxvid). A challenge loop usually means two identities. Confirm the container is supported. SFSafariViewController and Chrome Custom Tabs are not default hybrid.

Check that the app is deleting cookies or storage (logout, “clear site data”, WebView data APIs). Skip HUMAN keys and use SDK 5.3.0 or later so recovery can restore _pxmd / _pxwvm. Also confirm you did not list a sub-domain as the only webRootDomains entry.

Expected. The first navigation after a cold start can run before the SDK has a VID to publish. Later navigations should match. This is an accepted first-launch limitation, not a domain-config bug.

Apple Pay on the Web

If your website uses Apple Pay on the Web, disable JavaScript evaluation by the SDK. Apple Pay cannot be used alongside script injection APIs on iOS 13–15.

If your app targets only iOS 16 or above, you do not need to disable JavaScript evaluation.

Swift:

policy.hybridAppPolicy.allowJavaScriptEvaluation = false

Objective-C:

policy.hybridAppPolicy.allowJavaScriptEvaluation = NO;

External Web Views (available from v4.0.1)

If WebViews are created outside native code but still use WKWebView or Android WebView (for example react-native-webview), enable supportExternalWebViews.

The HUMAN React Native wrapper and Expo module already set supportExternalWebViews = true. Flutter, Ionic, Capacitor, and Cordova do not. Instead, set the flag in native start together with webRootDomains. See WebView container support for more information.

Android

Kotlin:

policy.hybridAppPolicy.supportExternalWebViews = true

Java:

policy.getHybridAppPolicy().setSupportExternalWebViews(true);

iOS

Swift:

policy.hybridAppPolicy.supportExternalWebViews = true

Objective-C:

policy.hybridAppPolicy.supportExternalWebViews = YES;