> This page is for Applications Protection.

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.humansecurity.com/llms.txt.

# 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.

> **Note**
>
> 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](/applications/webview-container-support-v5) matrix.

[`SFSafariViewController`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller) and [Chrome Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs) need a **special integration**. Those containers own a cookie jar the app cannot write with the default Hybrid App path. See [External browser integration](#external-browser-integration) for details.

## 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 host        | `webRootDomains = ["example.com"]` |
| ------------------- | ---------------------------------- |
| `example.com`       | Match                              |
| `www.example.com`   | Match                              |
| `api.example.com`   | Match                              |
| `login.example.com` | Match                              |
| `notexample.com`    | **No 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.

> **Warning**
>
> 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.

### Cookie `Domain` versus test harnesses

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.

**`Kotlin`**

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

**`Swift`**

```swift title="Swift"
let policy = HSPolicy()
policy.hybridAppPolicy.set(webRootDomains: ["example.com"], forAppId: "PX123")
policy.hybridAppPolicy.set(webRootDomains: ["other-shop.com"], forAppId: "PX456")
try HumanSecurity.start(appIds: ["PX123", "PX456"], policy: 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](/applications/native-easiest-implementation-v5)
2. [Basic Implementation](/applications/native-basic-implementation-v5)
3. [Advanced Functionality](/applications/native-advanced-functionality-v5)

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:**

```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:**

```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):**

```kotlin
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):**

```java
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:**

```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:**

```objc
@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:

| Side    | VID and where to read                                                              |
| ------- | ---------------------------------------------------------------------------------- |
| Native  | Read `X-PX-VID` on a request your app sends to a protected host (from SDK headers) |
| WebView | Read `_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:

| Name     | Writer     | Role                                                                                                                                                                                |
| -------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `_pxmd`  | Mobile SDK | Payload 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. |
| `_pxwvm` | Mobile SDK | Marks the page as an in-app WebView                                                                                                                                                 |
| `_pxvid` | Sensor     | Visitor 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](#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](/applications/how-to-verify-the-sdk-integration-in-your-app-v5) 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](/applications/use-of-cookies-web-storage).

## Troubleshooting

#### Native VID and WebView VID differ

**First check:** `webRootDomains` is the bare root (`example.com`), not a sub-domain. Confirm matching with the [table above](#how-matching-works).

Then check the following:

1. The WebView is a supported container
2. `setupWebView` / `automaticSetup` / `supportExternalWebViews` matches how the WebView is created
3. [Contact HUMAN](/applications/human-customer-support) if `_pxhd` is on a domain shared with desktop.

#### Challenge loop inside a WebView

Check the VID split (native vs. `_pxvid`). A challenge loop usually means two identities. Confirm the container is supported. [`SFSafariViewController`](https://developer.apple.com/documentation/safariservices/sfsafariviewcontroller) and [Chrome Custom Tabs](https://developer.chrome.com/docs/android/custom-tabs) require [external browser integration](#external-browser-integration).

#### VID changes between navigations

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.

#### VIDs differ only on the first navigation after a cold start

**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](https://developer.apple.com/documentation/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:**

```swift
policy.hybridAppPolicy.allowJavaScriptEvaluation = false
```

**Objective-C:**

```objc
policy.hybridAppPolicy.allowJavaScriptEvaluation = NO;
```

## External browser integration

When embedding web content using `SFSafariViewController` on iOS or **Chrome Custom Tabs** on Android, those components run in a separate process and use an isolated cookie jar that the SDK cannot access directly.

To maintain Visitor ID (VID) alignment between your native app and the web sessions inside these external browsers, you must explicitly pass the SDK's VID in the URL query parameters.

### Requirements

1. The HUMAN Mobile SDK must be initialized and running.
2. **Backend configuration:** You must contact HUMAN Support or your Technical Account Manager (TAM) to enable the **"Extract VID from URL"** configuration for your App ID on the backend. Without this server-side configuration, the embedded web sensor ignores the URL parameter.

### Important timing considerations

The SDK retrieves its Visitor ID asynchronously from the HUMAN Collector during the startup flow (`HumanSecurity.start()`).

This means the VID getter may return `nil` (or `null`) if you attempt to launch the external browser immediately on application launch, before the SDK has completed its initial handshake.

> **Note**
>
> If your application flow requires opening the browser immediately upon app launch and the VID is not ready in time, contact HUMAN Support for a custom solution.

### iOS implementation

To pass the VID on iOS, append it as the `_pxvid` query parameter to your URL before presenting the `SFSafariViewController`.

**Swift:**

```swift
import SafariServices
import HUMAN

func openProtectedWebsite(appId: String) {
    // 1. Get your target URL
    var components = URLComponents(string: "https://your-protected-website.com")
    
    // 2. Retrieve the VID from the SDK. 
    // Note: This may be nil if the SDK hasn't finished its initial sync.
    if let vid = HumanSecurity.vid(forAppId: appId) {
        // 3. Append the _pxvid query parameter
        components?.queryItems = [URLQueryItem(name: "_pxvid", value: vid)]
    } else {
        print("HUMAN VID is not yet available.")
    }
    
    // 4. Present the Safari View Controller
    guard let url = components?.url else { return }
    let safariViewController = SFSafariViewController(url: url)
    
    // Present the view controller from your current context
    // present(safariViewController, animated: true)
}
```

### Android implementation

To pass the VID on Android, append it as the `_pxvid` query parameter to your `Uri` before launching the Custom Tab.

**Kotlin:**

```kotlin
import android.content.Context
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import com.humansecurity.mobile_sdk.HumanSecurity

fun openProtectedWebsite(context: Context, appId: String) {
    val uriBuilder = Uri.parse("https://your-protected-website.com").buildUpon()
    
    // 1. Retrieve the VID from the SDK.
    // Note: This may be null if the SDK hasn't finished its initial sync.
    val vid = HumanSecurity.vid(appId)
    
    // 2. Append the _pxvid query parameter if available
    if (!vid.isNullOrEmpty()) {
        uriBuilder.appendQueryParameter("_pxvid", vid)
    } else {
        println("HUMAN VID is not yet available.")
    }
    
    // 3. Launch the Custom Tab
    val uri = uriBuilder.build()
    CustomTabsIntent.Builder()
        .build()
        .launchUrl(context, uri)
}
```

## 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](https://github.com/react-native-webview/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](/applications/webview-container-support-v5) for more information.

### Android

**Kotlin:**

```kotlin
policy.hybridAppPolicy.supportExternalWebViews = true
```

**Java:**

```java
policy.getHybridAppPolicy().setSupportExternalWebViews(true);
```

### iOS

**Swift:**

```swift
policy.hybridAppPolicy.supportExternalWebViews = true
```

**Objective-C:**

```objc
policy.hybridAppPolicy.supportExternalWebViews = YES;
```