> ## Documentation Index
> Fetch the complete documentation index at: https://docs.go.gbgplc.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API reference

> Complete type reference for the GBGBridge Android SDK.

Complete reference for every public type, method, and property in the GBGBridge Android SDK.

**Artifact:** `com.gbg:gbgbridge-sdk:0.1.0-alpha01` ([Maven Central](https://central.sonatype.com/artifact/com.gbg/gbgbridge-sdk))
**Packages:** `com.gbg.gbgbridge.core`, `com.gbg.gbgbridge.models`, `com.gbg.gbgbridge.capabilities`, `com.gbg.gbgbridge.webview`
**Platforms:** Android API 24+ (Android 7.0)
**Kotlin:** 2.x

***

## Table of contents

* [Primary entry points](#primary-entry-points)
  * [BridgeHost](#bridgehost)
  * [BridgeWebViewConfigurator](#bridgewebviewconfigurator)
* [Capabilities](#capabilities)
  * [CaptureCapability](#capturecapability)
  * [CaptureResult](#captureresult)
  * [PermissionState](#permissionstate)
  * [CameraDetector](#cameradetector)
* [Configuration](#configuration)
  * [BridgeConfiguration](#bridgeconfiguration)
  * [BridgeCapabilityInfo](#bridgecapabilityinfo)
* [Message models](#message-models)
  * [BridgeMessage](#bridgemessage)
  * [BridgePayload](#bridgepayload)
  * [BridgeErrorPayload](#bridgeerrorpayload)
  * [BridgeRequestOptions](#bridgerequestoptions)
  * [BridgeMessageType](#bridgemessagetype)
  * [BridgeResponseStatus](#bridgeresponsestatus)
  * [JsonElement](#jsonelement)
* [Interfaces](#interfaces)
  * [BridgeCapabilityHandler](#bridgecapabilityhandler)
  * [BridgeResponder](#bridgeresponder)
  * [BridgeHostDelegate](#bridgehostdelegate)
* [Built-in handlers](#built-in-handlers)
  * [CapabilityQueryHandler](#capabilityqueryhandler)

***

## Primary entry points

These are the types you'll interact with directly to wire the bridge into your app: a host that routes messages, and a configurator that prepares a `WebView` for bridge traffic.

<Note>**Differs from iOS:** there is no `BridgeWebView` composable or `makeWebView` factory on Android. Integration is a plain `WebView(context)` (or `AndroidView { WebView(it) }` in Compose) plus `host.attach(webView)` plus `webView.loadUrl(journeyUrl)`. See [Embedding the WebView](/docs/go-v2/developer-integration/sdks/android/embedding) for the full pattern.</Note>

### BridgeHost

The central coordinator that manages message routing between a `WebView` and your native code.

```kotlin theme={null}
package com.gbg.gbgbridge.core

public class BridgeHost(
  public val configuration: BridgeConfiguration,
  capabilitiesProvider: (() -> Map<String, BridgeCapabilityInfo>)? = null,
)
```

**Why it exists:** `BridgeHost` is the main object you interact with. It decodes incoming messages from the WebView, routes requests to registered handlers, tracks pending requests, and sends responses and events back to the web journey.

**Threading:** Main-thread-only. Every public state-mutating method asserts the main thread at runtime and throws `IllegalStateException` if called from a background thread; the `@MainThread` annotation also enforces this statically via lint. Inbound JavaScript messages arrive on the WebView render thread and are posted to the main looper before any handler or delegate runs.

<Note>**Differs from iOS:** iOS enforces main-thread isolation through `@MainActor` and the Swift concurrency system. Android relies on a documented main-thread contract enforced with runtime assertions and lint — the compiler will not stop you, but the SDK will fail loudly rather than corrupt state silently.</Note>

#### Constructors

`BridgeHost` offers two constructors. Most apps should use the `hostVersion` convenience constructor, which wires up typed capability slots automatically. Use the primary constructor when you need to declare capabilities explicitly or supply a dynamic provider.

##### BridgeHost(hostVersion:) — Recommended

```kotlin theme={null}
public constructor(hostVersion: String)
```

Creates a new bridge host with typed capability slots and an empty static capability map. Capability support is declared by setting handlers on the `documentCapture` and `selfieCapture` slots. A `CapabilityQueryHandler` is registered automatically and builds its response dynamically from typed slots and custom capabilities, including `permissionState` metadata.

| Parameter     | Type     | Description                                          |
| ------------- | -------- | ---------------------------------------------------- |
| `hostVersion` | `String` | A version string for the host app (e.g., `"1.0.0"`). |

**Example:**

```kotlin theme={null}
val host = BridgeHost(hostVersion = "1.0.0")

// Declare support by setting handlers
host.documentCapture.handler = { request ->
  host.documentCapture.awaitCompletion()
}
```

***

##### BridgeHost(configuration:, capabilitiesProvider:) — Configuration-based

```kotlin theme={null}
public constructor(
  configuration: BridgeConfiguration,
  capabilitiesProvider: (() -> Map<String, BridgeCapabilityInfo>)? = null,
)
```

Creates a new bridge host with explicit configuration. Capabilities are declared statically in the `BridgeConfiguration` map, or dynamically via `capabilitiesProvider`. A built-in `CapabilityQueryHandler` is registered automatically for `"capability.query"` requests.

| Parameter              | Type                                         | Default | Description                                                                                                                                                                                 |
| ---------------------- | -------------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `configuration`        | `BridgeConfiguration`                        | —       | Declares the host version, capability map, bootstrap script, and origin allowlist.                                                                                                          |
| `capabilitiesProvider` | `(() -> Map<String, BridgeCapabilityInfo>)?` | `null`  | Optional dynamic capability provider. When supplied, it replaces the static configuration map and is re-evaluated on every capability read, so capability state can change without polling. |

<Note>**Differs from iOS:** `capabilitiesProvider` is an Android-only addition. On iOS, dynamic capability state is expressed by mutating the published `capabilities` map; on Android the merged `capabilities` property is read-only, and dynamic state belongs in the provider. Also unlike iOS — where `init(configuration:)` does not wire typed slots into the query handler — both Android constructors merge configured typed slots into `capability.query` responses automatically.</Note>

**Example:**

```kotlin theme={null}
val host = BridgeHost(
  configuration = BridgeConfiguration(
    hostVersion = "1.0.0",
    capabilities = mapOf(
      "camera.document" to BridgeCapabilityInfo(supported = true, version = "1.0"),
    ),
  ),
)
```

**Example — dynamic provider:**

```kotlin theme={null}
val host = BridgeHost(
  configuration = BridgeConfiguration(hostVersion = "1.0.0"),
  capabilitiesProvider = {
    mapOf(
      "nfc.read" to BridgeCapabilityInfo(
        supported = nfcAdapter?.isEnabled == true,
        version = "1.0",
      ),
    )
  },
)
```

<Warning>The static `configuration.capabilities` map is defensively snapshotted at host construction. Mutating a `MutableMap` you passed in does **not** change query responses — use `capabilitiesProvider` for dynamic state. If your provider lambda can throw, wrap direct reads of `host.capabilities`; a provider throw propagates to the caller on direct reads (the inbound `capability.query` path is protected separately by the SDK's handler catch).</Warning>

#### Companion constants

These constants describe the protocol identity and the host's internal buffer limits. The buffer caps surface through public behaviour — eviction from `receivedMessages`, leak warnings in `lastError`, and the response dedupe window.

| Constant                         | Value       | Description                                                                                                                                                   |
| -------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PROTOCOL_VERSION`               | `"1.0"`     | The bridge protocol version stamped on every outbound message.                                                                                                |
| `ENVIRONMENT`                    | `"android"` | The environment token reported in `capability.query` responses.                                                                                               |
| `MAX_RECEIVED_MESSAGES`          | `200`       | Cap on the `receivedMessages` inbound buffer. Older entries are evicted from the head when exceeded.                                                          |
| `PENDING_REQUEST_LEAK_THRESHOLD` | `50`        | When `pendingRequests` grows beyond this size, `lastError` is set with a leak warning.                                                                        |
| `MAX_RESPONDED_TRACKED`          | `200`       | How many recently responded correlation IDs the host remembers to deduplicate explicit-action `respond` calls. Oldest entries are evicted in insertion order. |

#### Properties

These properties expose the host's configuration and observable state. The list-valued properties return immutable snapshots — each read copies the underlying buffer.

***

##### configuration

```kotlin theme={null}
public val configuration: BridgeConfiguration
```

The configuration passed at construction. Read-only.

***

##### delegate

```kotlin theme={null}
public var delegate: BridgeHostDelegate?
```

An optional delegate that receives notifications about inbound messages, outbound messages, unhandled requests, and errors. Set this to observe bridge activity without registering handlers. Read and assigned on the main thread.

<Warning>**Differs from iOS:** the delegate is backed by a `WeakReference` — the host does **not** keep your delegate alive. The common pattern (the delegate is your Activity, Fragment, or ViewModel, which the platform holds strongly) is unaffected, but an inline assignment like `host.delegate = SomeImpl()` with no other strong reference will be garbage-collected, after which callbacks silently stop firing. Always hold your own strong reference to the delegate.</Warning>

***

##### capabilities

```kotlin theme={null}
public val capabilities: Map<String, BridgeCapabilityInfo>
```

A read-only merged snapshot of the host's capability map, computed on each read. The merge combines, from lowest to highest precedence (later entries win on key collision):

1. Runtime `registerCustomCapability` registrations.
2. The static configuration map or the dynamic `capabilitiesProvider` — explicit configuration is authoritative over custom registrations.
3. Typed slots (`documentCapture`, `selfieCapture`) **with a non-null handler**. An unused slot never shadows integrator-supplied capability info for the same ID.

<Note>**Differs from iOS:** iOS exposes a mutable `@Published var capabilities: [String: Bool]` that hosts write directly. The Android property is read-only and richer (`BridgeCapabilityInfo` values, not booleans); dynamic state belongs in `capabilitiesProvider`.</Note>

***

##### receivedMessages

```kotlin theme={null}
public val receivedMessages: List<BridgeMessage>
```

An ordered snapshot of all messages received from the web journey (requests, responses, and events), capped at `MAX_RECEIVED_MESSAGES` (200) with oldest entries evicted first. Each read returns an immutable copy. Useful for debugging or building a message log UI. Read on the main thread.

***

##### pendingRequests

```kotlin theme={null}
public val pendingRequests: List<BridgeMessage>
```

A snapshot of requests from the web journey that have no registered handler. These are waiting for a manual response via the lookup overload of [`respond`](#respondto-status-data-error). When a response is sent for a pending request, it is removed from this list. If the list grows beyond `PENDING_REQUEST_LEAK_THRESHOLD` (50), `lastError` is set with a leak warning. Read on the main thread.

***

##### lastError

```kotlin theme={null}
public var lastError: String? // private set
```

The most recent error message, if any. Set when message decoding fails, outbound encoding fails, a handler throws before responding, an origin-gated message is rejected, or the pending-request buffer exceeds its leak threshold. Call `clearError()` to reset.

<Note>**Differs from iOS:** the setter is private — host apps cannot write `lastError` themselves; only the SDK records errors there. Also, sending while no WebView is attached does **not** set `lastError` on Android (see [sendEvent](#sendeventaction-data)), whereas iOS records `"WebView not attached"`.</Note>

***

##### documentCapture

```kotlin theme={null}
public val documentCapture: CaptureCapability
```

The typed slot for document capture (capability ID `camera.document`). Set its `handler` to declare support. Routes requests for the `"camera.document.capture"` action.

***

##### selfieCapture

```kotlin theme={null}
public val selfieCapture: CaptureCapability
```

The typed slot for selfie capture (capability ID `camera.selfie`). Set its `handler` to declare support. Routes requests for the `"camera.selfie.capture"` action.

***

#### Methods

All methods are main-thread-only and return `Unit` unless noted. After `dispose()`, the state-mutating methods throw `IllegalStateException` — see [dispose()](#dispose) for the full post-dispose contract.

***

##### attach(webView:, client:)

```kotlin theme={null}
public fun attach(
  webView: WebView,
  client: BridgeWebViewConfigurator.BootstrapInjectingWebViewClient? = null,
)
```

Associates a `WebView` with this host. Internally calls `BridgeWebViewConfigurator.configure(...)` (enabling JavaScript and DOM storage, and installing the bootstrap-injecting `WebViewClient`), then installs the SDK's JavaScript interface on the WebView under the name `GBGBridge`. If the host is already attached, it detaches first.

| Parameter | Type                               | Default | Description                                                                                                                                                                                                                                                                               |
| --------- | ---------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `webView` | `WebView`                          | —       | The WebView to attach. The host holds a weak reference.                                                                                                                                                                                                                                   |
| `client`  | `BootstrapInjectingWebViewClient?` | `null`  | Optional `WebViewClient` subclass to install alongside the JS interface. Hosts that need their own navigation policy (URL allowlisting, error handling) **must** pass their subclass here — assigning a different `WebViewClient` after `attach` works but loses the bootstrap injection. |

**Example:**

```kotlin theme={null}
val webView = WebView(context)
host.attach(webView)
webView.loadUrl(journeyUrl)
```

<Note>`attach()` calls `BridgeWebViewConfigurator.configure()` for you — do not call `configure()` separately before `attach()`, as it would be re-run and clobbered. If you need a custom `WebChromeClient`, set it **after** `attach()`.</Note>

***

##### detach()

```kotlin theme={null}
public fun detach()
```

Disconnects the host from its WebView. Cancels any in-flight typed-slot capture (so the web side receives a `cancelled` response), removes the JavaScript interface, and resets the per-attach-session state: the response dedupe set, `pendingRequests`, and `receivedMessages` are all cleared. Idempotent — safe to call when not attached.

<Note>**Differs from iOS:** iOS preserves `pendingRequests` and `receivedMessages` across detach. Android clears both, because a Compose host driving a list off these snapshots would otherwise show ghost entries from a previous attach-session after re-attaching to a different WebView.</Note>

***

##### dispose()

```kotlin theme={null}
public fun dispose()
```

Terminal teardown: detaches from the WebView and cancels the typed-slot coroutine scopes so future capture launches refuse rather than hang. Idempotent on repeat calls. Call once from the owner's terminal lifecycle hook (`Activity.onDestroy`, `ViewModel.onCleared`).

After `dispose()`:

* `attach`, `register`, `unregister`, `registerCustomCapability`, both `respond` overloads, and `sendEvent` throw `IllegalStateException`.
* `detach()`, `dispose()`, `clearError()`, all property getters (which return empty/`null`), and the `delegate` setter remain safe to call.
* The host must not be re-attached — hosts that just want to swap WebViews should use `detach()`/`attach()`, which keep the slot scopes live.

<Note>**Differs from iOS:** there is no iOS equivalent — ARC handles terminal cleanup there. On Android, dispose explicitly cancels the coroutine scopes that pin handler closures (and through them, Activity context).</Note>

<Warning>`detach()` failures propagate out of `dispose()`. The rare path is `removeJavascriptInterface` throwing on a WebView already in shutdown. Wrap the call when wiring it into `onDestroy`:</Warning>

```kotlin theme={null}
override fun onDestroy() {
  try {
    bridgeHost.dispose()
  } catch (e: Exception) {
    Log.w("Bridge", "dispose() failed during teardown", e)
  }
  super.onDestroy()
}
```

***

##### register(handler:)

```kotlin theme={null}
public fun register(handler: BridgeCapabilityHandler)
```

Registers a capability handler. When a request arrives with an `action` matching the handler's `action` property, the handler's `handle(request, responder)` method is called on the main thread.

If a handler is already registered for the same action, it is replaced — including the SDK's own auto-registered handlers (the `CapabilityQueryHandler` for `"capability.query"`, and the typed-slot handlers for the capture actions).

| Parameter | Type                      | Description              |
| --------- | ------------------------- | ------------------------ |
| `handler` | `BridgeCapabilityHandler` | The handler to register. |

**Example:**

```kotlin theme={null}
host.register(DocumentCaptureHandler())
```

***

##### unregister(action:)

```kotlin theme={null}
public fun unregister(action: String)
```

Removes the handler registered for the given action, along with any custom capability registered under the same name. Subsequent requests for that action are added to `pendingRequests` instead.

| Parameter | Type     | Description                          |
| --------- | -------- | ------------------------------------ |
| `action`  | `String` | The action identifier to unregister. |

<Note>`"capability.query"` is the action of the SDK's auto-registered `CapabilityQueryHandler`. Unregistering it makes capability queries fall through to the unhandled-request path; re-register a replacement (`CapabilityQueryHandler` is public for exactly this purpose) if you want the action to keep working.</Note>

***

##### registerCustomCapability(action:, version:, handler:)

```kotlin theme={null}
public fun registerCustomCapability(
  action: String,
  version: String? = null,
  handler: (BridgeMessage, BridgeResponder) -> Unit,
)
```

Registers a lightweight custom capability backed by a lambda. The capability automatically appears in `capability.query` responses with `supported = true` and the supplied version (defaulting to `"1.0"`). The lambda is invoked on the main thread when a matching request arrives. Exceptions thrown from the lambda are caught by the SDK and routed to `delegate.onError`, with a best-effort error response dispatched to the web side.

If a typed slot with a handler uses the same capability ID, the typed slot's info wins in query responses.

| Parameter | Type                                       | Default | Description                                                      |
| --------- | ------------------------------------------ | ------- | ---------------------------------------------------------------- |
| `action`  | `String`                                   | —       | The capability/action identifier (e.g., `"nfc.read"`).           |
| `version` | `String?`                                  | `null`  | Optional version string. Defaults to `"1.0"` in query responses. |
| `handler` | `(BridgeMessage, BridgeResponder) -> Unit` | —       | The lambda called when a matching request arrives.               |

<Note>**Differs from iOS:** the handler lambda is synchronous, not `async`. For asynchronous work, retain the responder, complete the work, hop back to the main thread, then call `responder.respond(...)` — see [BridgeResponder](#bridgeresponder).</Note>

**Example:**

```kotlin theme={null}
host.registerCustomCapability("nfc.read", version = "1.0") { request, responder ->
  // Handle NFC read request
  responder.respond(
    status = BridgeResponseStatus.SUCCESS,
    data = mapOf("chipRead" to JsonPrimitive(true)),
  )
}
```

***

##### sendEvent(action:, data:)

```kotlin theme={null}
public fun sendEvent(action: String, data: Map<String, JsonElement>? = null)
```

Sends a one-way event message to the web journey. Events are fire-and-forget — no response is expected. Each event is stamped with an auto-generated correlation ID of the form `android-event-{uuid}` (iOS uses `ios-event-{uuid}`).

| Parameter | Type                        | Default | Description                                               |
| --------- | --------------------------- | ------- | --------------------------------------------------------- |
| `action`  | `String`                    | —       | The event action identifier (e.g., `"journey.progress"`). |
| `data`    | `Map<String, JsonElement>?` | `null`  | Optional event payload data.                              |

**Example:**

```kotlin theme={null}
host.sendEvent(
  action = "host.ready",
  data = mapOf("timestamp" to JsonPrimitive(System.currentTimeMillis())),
)
```

**Error cases:** if outbound JSON encoding fails, `lastError` is set, `delegate.onError` fires, and the message is dropped.

<Note>**Differs from iOS:** if no WebView is attached, the message fires `delegate.onMessageSent` (recording intent for tracing) and is then silently dropped at transport — `lastError` is **not** set. iOS records `lastError = "WebView not attached"` in the same situation.</Note>

***

##### respond(to:, status:, data:, error:)

```kotlin theme={null}
public fun respond(
  to: String,
  status: BridgeResponseStatus,
  data: Map<String, JsonElement>? = null,
  error: BridgeErrorPayload? = null,
)
```

Sends a response to a pending request, looking up its action from `pendingRequests`. The matching pending request is removed before dispatch; the response's `action` is automatically set to the original request's action. If no pending request matches the correlation ID, the call silently no-ops.

| Parameter | Type                        | Default | Description                                                      |
| --------- | --------------------------- | ------- | ---------------------------------------------------------------- |
| `to`      | `String`                    | —       | The correlation ID of the request being responded to.            |
| `status`  | `BridgeResponseStatus`      | —       | The response status.                                             |
| `data`    | `Map<String, JsonElement>?` | `null`  | Optional response data.                                          |
| `error`   | `BridgeErrorPayload?`       | `null`  | Optional error details (typically set when `status` is `ERROR`). |

**Example:**

```kotlin theme={null}
host.respond(
  to = request.correlationId,
  status = BridgeResponseStatus.SUCCESS,
  data = mapOf("imageBase64" to JsonPrimitive(base64Image)),
)
```

<Warning>**Retry asymmetry:** the pending entry is consumed before sending. If the send then fails (encode error, or no WebView attached), a retry through this lookup overload silently no-ops because the entry is gone. To retry, use the explicit-action overload below — you can recover the original action from the request you captured via `delegate.onUnhandledRequest`.</Warning>

***

##### respond(to:, action:, status:, data:, error:)

```kotlin theme={null}
public fun respond(
  to: String,
  action: String,
  status: BridgeResponseStatus,
  data: Map<String, JsonElement>? = null,
  error: BridgeErrorPayload? = null,
)
```

Sends a response with an explicit action, without requiring a matching pending request. Use this from contexts that have already consumed the request, or when retrying after a failed send.

This overload deduplicates by correlation ID: a second call with the same ID silently no-ops (the dedupe window holds the most recent `MAX_RESPONDED_TRACKED` = 200 IDs and is reset on `detach()`). If the send fails — encode error or no WebView attached — the dedupe entry is rolled back so a retry with the same correlation ID works once the underlying problem is resolved.

| Parameter | Type                        | Default | Description                             |
| --------- | --------------------------- | ------- | --------------------------------------- |
| `to`      | `String`                    | —       | The correlation ID of the request.      |
| `action`  | `String`                    | —       | The action identifier for the response. |
| `status`  | `BridgeResponseStatus`      | —       | The response status.                    |
| `data`    | `Map<String, JsonElement>?` | `null`  | Optional response data.                 |
| `error`   | `BridgeErrorPayload?`       | `null`  | Optional error details.                 |

***

##### clearError()

```kotlin theme={null}
public fun clearError()
```

Resets `lastError` to `null`. Safe to call after `dispose()`.

***

#### Request dispatch behaviour

When a request arrives with no registered handler, it is appended to `pendingRequests` and `delegate.onUnhandledRequest` fires; respond later via the lookup `respond` overload. When a registered handler throws, the SDK catches the exception and routes it to `delegate.onError`. If the handler had not yet responded, `lastError` is also set and an `ERROR` response with code `HANDLER_FAILURE` (`recoverable = false`) is dispatched to the web side; a handler that responded successfully and *then* threw is reported via `onError` only — the request itself still succeeded.

See [Messaging](/docs/go-v2/developer-integration/sdks/android/messaging) for the full message flow and [Capability handling](/docs/go-v2/developer-integration/sdks/android/capability-handling) for handler patterns.

***

### BridgeWebViewConfigurator

A utility object that configures a `WebView` with the bridge infrastructure. `BridgeHost.attach()` calls it for you — you only interact with it directly when subclassing its `WebViewClient` for custom navigation policy.

```kotlin theme={null}
package com.gbg.gbgbridge.webview

public object BridgeWebViewConfigurator
```

**Why it exists:** Separates WebView setup (JavaScript settings, bootstrap injection, client installation) from the host's message routing.

#### configure(webView:, bootstrapScript:, client:)

```kotlin theme={null}
public fun configure(
  webView: WebView,
  bootstrapScript: String? = null,
  client: BootstrapInjectingWebViewClient? = null,
)
```

Applies the SDK's required WebView defaults and installs a `WebViewClient` that injects the bootstrap script on each page load:

1. Sets `javaScriptEnabled = true` and `domStorageEnabled = true`.
2. Installs a `BootstrapInjectingWebViewClient` (the supplied `client`, or a default built from `bootstrapScript`).
3. Installs a plain `WebChromeClient`.

| Parameter         | Type                               | Default | Description                                                                                                                                                                                                                           |
| ----------------- | ---------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `webView`         | `WebView`                          | —       | The WebView to configure.                                                                                                                                                                                                             |
| `bootstrapScript` | `String?`                          | `null`  | Optional override of the JS bootstrap injected on every page load. `null` uses the default script (below). **Ignored when `client` is non-null** — the subclass owns its script; pass the script to the subclass constructor instead. |
| `client`          | `BootstrapInjectingWebViewClient?` | `null`  | Optional pre-built `WebViewClient` subclass, installed as-is.                                                                                                                                                                         |

The default bootstrap script is:

```javascript theme={null}
window.GBGBridge = window.GBGBridge || {}; window.GBGBridge.receive = window.GBGBridge.receive || function(){};
```

<Warning>`configure()` unconditionally **overwrites** any existing `webViewClient` and `webChromeClient` on the WebView (the API-26+ getters that would allow reading an existing client are unavailable at the SDK's `minSdk = 24` floor). Since `BridgeHost.attach()` calls `configure()` internally, set a custom `WebChromeClient` only **after** `attach()`, and supply custom `WebViewClient` behaviour by subclassing `BootstrapInjectingWebViewClient` and passing it to `attach(webView, client = ...)`.</Warning>

#### BootstrapInjectingWebViewClient

This nested class is the SDK's `WebViewClient`. It is `public open` so security-conscious hosts can subclass it to layer their own navigation policy without losing the bootstrap injection that makes the bridge work.

```kotlin theme={null}
public open class BootstrapInjectingWebViewClient(
  private val bootstrapScript: String,
) : WebViewClient()
```

The client injects the bootstrap via `evaluateJavascript` in `onPageStarted` (main frame only — sub-frames receive no bootstrap, matching iOS's `forMainFrameOnly: true`). Override any callback you need — `shouldOverrideUrlLoading` allowlists, error logging, SSL handling — and call `super.onPageStarted(...)` to preserve the bootstrap.

**Example — restricting navigation to a single origin:**

```kotlin theme={null}
class JourneyClient(bootstrap: String) : BootstrapInjectingWebViewClient(bootstrap) {
  override fun shouldOverrideUrlLoading(
    view: WebView, request: WebResourceRequest
  ): Boolean {
    val url = request.url ?: return true
    return url.host != "journey.example.com"
  }
}

host.attach(webView, client = JourneyClient(myBootstrap))
```

<Note>**Differs from iOS:** iOS injects the bootstrap with `WKUserScript(.atDocumentStart)`, which is guaranteed to run before any page JavaScript. Android's `onPageStarted` + `evaluateJavascript` is best-effort — a `<script>` in the document head that synchronously calls `window.GBGBridge.receive` could race the injection. Hosts that install their own client must also supply the bootstrap literal themselves (via the subclass constructor), since the default script constant is not public.</Note>

***

## Capabilities

The capability types implement the native side of capability negotiation: typed slots that declare and route capture requests, a strongly-typed result hierarchy, and helpers for permission state.

### CaptureCapability

A typed capability slot that represents a capture operation (document or selfie). Setting a `handler` declares support; the SDK handles routing, result encoding, and busy rejection automatically.

```kotlin theme={null}
package com.gbg.gbgbridge.capabilities

public class CaptureCapability(
  public val id: String,
  public val actionId: String,
  initialVersion: String? = null,
)
```

**Why it exists:** Eliminates the need to manually build `BridgeCapabilityInfo` maps, encode capture results into `JsonElement`, and keep capability declarations in sync with handler registrations. Setting a handler *is* the declaration.

You typically don't create `CaptureCapability` instances directly — use the built-in slots on `BridgeHost` (`documentCapture`, `selfieCapture`). All mutable state is main-thread-only, like the rest of the SDK.

#### Properties

| Property          | Type                                          | Description                                                                                                                                                                                                                                                            |
| ----------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`              | `String`                                      | The capability identifier (e.g., `"camera.document"`).                                                                                                                                                                                                                 |
| `actionId`        | `String`                                      | The bridge action this slot handles (e.g., `"camera.document.capture"`).                                                                                                                                                                                               |
| `handler`         | `(suspend (BridgeMessage) -> CaptureResult)?` | The handler suspend lambda, invoked on `Dispatchers.Main`. Setting this declares support.                                                                                                                                                                              |
| `isEnabled`       | `Boolean`                                     | Runtime toggle. When `false`, the slot reports as unsupported in `capability.query` even if a handler is set. Default: `true`. **Advisory only** — it does not gate dispatch; a request arriving while `isEnabled` is false still routes to the handler (matches iOS). |
| `isSupported`     | `Boolean`                                     | Computed: `handler != null && isEnabled`.                                                                                                                                                                                                                              |
| `activeRequest`   | `StateFlow<BridgeMessage?>`                   | The currently in-flight request, or `null` when idle. In Compose, observe with `collectAsState()`; in the View system, `launchIn` a lifecycle scope.                                                                                                                   |
| `permissionState` | `PermissionState`                             | The current permission state for this capability, included in query responses. Default: `PermissionState.NOT_DETERMINED`.                                                                                                                                              |
| `version`         | `String?`                                     | Optional version string. Defaults to `"1.0"` in query responses when supported.                                                                                                                                                                                        |

<Note>**Differs from iOS:** the handler is a Kotlin suspend lambda rather than a Swift `async` closure, and `activeRequest` is a `StateFlow` rather than an `@Published` property — the observation idiom changes but the semantics match.</Note>

#### Request dispatch

When a request arrives for the slot's `actionId`, the slot decides as follows:

1. No handler set → responds `UNSUPPORTED` (error code `UNSUPPORTED`).
2. A request is already in flight → responds `ERROR` with code `BUSY` (`recoverable = true`).
3. Otherwise, `activeRequest` is set and the handler is launched on the main dispatcher.
4. Handler throws → `HANDLER_FAILURE` response (`recoverable = false`) plus `delegate.onError`. A `CancellationException` is translated to a `cancelled` response instead.
5. Handler returns a `CaptureResult` → the result is encoded onto the wire and `activeRequest` is cleared.

#### Methods

These methods drive capability handling at runtime — building query responses, awaiting capture completion, and signalling completion or cancellation from the UI layer.

##### buildCapabilityInfo()

```kotlin theme={null}
public fun buildCapabilityInfo(): BridgeCapabilityInfo
```

Returns a `BridgeCapabilityInfo` reflecting the slot's current state: `supported` mirrors `isSupported`, `version` is `null` when unsupported (else falls back to `"1.0"`), and `permissionState` carries the slot's current wire token. Used internally to build `capability.query` responses.

***

##### awaitCompletion()

```kotlin theme={null}
public suspend fun awaitCompletion(): CaptureResult
```

Suspends the current handler until `complete(...)` is called (or returns `CaptureResult.Cancelled` if `cancelIfBusy` runs first). Use this in handlers that present capture UI declaratively and complete from a callback.

**Example:**

```kotlin theme={null}
host.documentCapture.handler = { request ->
  showCaptureUi()
  host.documentCapture.awaitCompletion()
}
```

***

##### complete(result:)

```kotlin theme={null}
public fun complete(result: CaptureResult)
```

Resumes the pending handler with a capture result. Call from your capture UI's callback. Main-thread-only; silently no-ops when no handler is pending.

**Example:**

```kotlin theme={null}
// In a camera capture callback (after hopping to the main thread):
host.documentCapture.complete(
  CaptureResult.Document(
    imageData = imageBytes,
    width = 1920,
    height = 1080,
  ),
)
```

***

##### cancelIfBusy(reason:)

```kotlin theme={null}
public fun cancelIfBusy(reason: String = "Request cancelled")
```

Cancels the in-flight request, if any: resumes the pending handler with `CaptureResult.Cancelled(reason)` and clears `activeRequest` immediately. Main-thread-only; safe to call when idle. Call this from teardown paths (Activity destruction, dialog dismissal) and when the user backs out of your capture UI.

See [Capture screens](/docs/go-v2/developer-integration/sdks/android/capture-screens) for complete capture-UI patterns built on this slot.

***

### CaptureResult

A strongly-typed result returned by capture handlers.

```kotlin theme={null}
public sealed class CaptureResult
```

**Why it exists:** Eliminates manual `JsonElement` map construction. The SDK converts `CaptureResult` values to the bridge protocol format automatically.

<Note>**Differs from iOS:** iOS models this as an enum with separate `DocumentCaptureResult` / `SelfieCaptureResult` wrapper structs. Kotlin uses a sealed class whose subclasses carry the data directly — construct `CaptureResult.Document(...)` rather than `.document(DocumentCaptureResult(...))`. Image payloads are `ByteArray` instead of `Data`; the `Document` and `Selfie` data classes override `equals`/`hashCode` to compare byte-array content. The wire format is identical on both platforms.</Note>

#### CaptureResult.Document

A successful document capture, carrying the encoded image bytes. The SDK performs no compression or conversion — the bytes are emitted as supplied.

```kotlin theme={null}
public data class Document(
  val imageData: ByteArray,
  val width: Int,
  val height: Int,
  val mimeType: String = "image/png",
) : CaptureResult()
```

| Parameter   | Type        | Default       | Description                                                                                |
| ----------- | ----------- | ------------- | ------------------------------------------------------------------------------------------ |
| `imageData` | `ByteArray` | —             | The captured image data. Base64-encoded in the bridge response.                            |
| `width`     | `Int`       | —             | Image width in pixels.                                                                     |
| `height`    | `Int`       | —             | Image height in pixels.                                                                    |
| `mimeType`  | `String`    | `"image/png"` | The MIME type of the image data. Supply `"image/jpeg"` if you compress a `Bitmap` to JPEG. |

**Bridge response data** (status `success`):

```json theme={null}
{
  "imageBase64": "<base64-encoded image>",
  "imageWidth": 1920,
  "imageHeight": 1080,
  "mimeType": "image/png"
}
```

#### CaptureResult.Selfie

A successful selfie capture, carrying preview image bytes and biometric blob bytes (encrypted and unencrypted, integrator-supplied).

```kotlin theme={null}
public data class Selfie(
  val previewImageData: ByteArray,
  val width: Int,
  val height: Int,
  val mimeType: String = "image/jpeg",
  val encryptedBlob: ByteArray,
  val unencryptedBlob: ByteArray,
) : CaptureResult()
```

| Parameter          | Type        | Default        | Description                                                             |
| ------------------ | ----------- | -------------- | ----------------------------------------------------------------------- |
| `previewImageData` | `ByteArray` | —              | Preview image data. Base64-encoded in the bridge response.              |
| `width`            | `Int`       | —              | Image width in pixels.                                                  |
| `height`           | `Int`       | —              | Image height in pixels.                                                 |
| `mimeType`         | `String`    | `"image/jpeg"` | The MIME type of the preview image.                                     |
| `encryptedBlob`    | `ByteArray` | —              | Encrypted biometric data blob. Base64-encoded in the bridge response.   |
| `unencryptedBlob`  | `ByteArray` | —              | Unencrypted biometric data blob. Base64-encoded in the bridge response. |

**Bridge response data** (status `success`):

```json theme={null}
{
  "imageBase64": "<base64>",
  "imageWidth": 640,
  "imageHeight": 480,
  "mimeType": "image/jpeg",
  "encryptedBlobBase64": "<base64>",
  "unencryptedBlobBase64": "<base64>"
}
```

#### CaptureResult.Cancelled

The capture was cancelled by the user or system. Produces a `cancelled` response with error code `CANCELLED` and `recoverable = true`.

```kotlin theme={null}
public data class Cancelled(val reason: String) : CaptureResult()
```

#### CaptureResult.Failed

The capture failed. Produces an `error` response carrying the supplied code, message, and recoverability flag in a `BridgeErrorPayload`.

```kotlin theme={null}
public data class Failed(
  val code: String,
  val message: String,
  val recoverable: Boolean,
) : CaptureResult()
```

***

### PermissionState

The current authorisation status for a native capability. The wire representation — the string surfaced in `capability.query` responses — uses the same tokens as iOS, exposed via the `wireValue` property.

```kotlin theme={null}
public enum class PermissionState(public val wireValue: String)
```

| Case             | Wire value        | Description                                                       |
| ---------------- | ----------------- | ----------------------------------------------------------------- |
| `GRANTED`        | `"granted"`       | Permission has been granted.                                      |
| `DENIED`         | `"denied"`        | Permission has been explicitly denied.                            |
| `NOT_DETERMINED` | `"notDetermined"` | Permission has not been requested yet.                            |
| `RESTRICTED`     | `"restricted"`    | Permission is restricted by device policy (e.g., a work profile). |
| `NOT_APPLICABLE` | `"notApplicable"` | Permission is not applicable for this capability.                 |

***

### CameraDetector

A utility for detecting camera hardware availability and permission status.

```kotlin theme={null}
public object CameraDetector
```

**Why it exists:** Provides a simple way to populate `permissionState` on capture slots without writing `PackageManager` boilerplate.

#### check(context:)

```kotlin theme={null}
public fun check(context: Context): CameraDetector.Result
```

Checks camera hardware availability using `PackageManager.FEATURE_CAMERA_ANY` and permission status using `Context.checkSelfPermission(Manifest.permission.CAMERA)`.

| Parameter | Type      | Description                         |
| --------- | --------- | ----------------------------------- |
| `context` | `Context` | Any context — no Activity required. |

**Returns:** A `CameraDetector.Result` with hardware and permission info.

#### CameraDetector.Result

The nested result type carries the two facts a host needs to seed its capture slots.

```kotlin theme={null}
public data class Result(
  val hardwareAvailable: Boolean,
  val permissionState: PermissionState,
)
```

| Property            | Type              | Description                                                                                             |
| ------------------- | ----------------- | ------------------------------------------------------------------------------------------------------- |
| `hardwareAvailable` | `Boolean`         | Whether a camera device is physically present. Typically `false` on emulators without a virtual camera. |
| `permissionState`   | `PermissionState` | The current camera permission state — see the caveat below.                                             |

<Note>**Differs from iOS:** the Android runtime permission API cannot reliably distinguish "never requested" from "permanently denied" without an Activity and integrator-tracked state, so `check()` reports only `GRANTED` vs `NOT_DETERMINED`. Integrators who have run their own permission flow should set the richer `DENIED`/`RESTRICTED` state on the slot directly. iOS reports the full range from `AVAuthorizationStatus`.</Note>

**Example:**

```kotlin theme={null}
val camera = CameraDetector.check(context)
host.documentCapture.permissionState = camera.permissionState
host.selfieCapture.permissionState = camera.permissionState
```

***

## Configuration

The configuration types describe what the host declares to the web journey: its version, its capability map, and the optional WebView bootstrap and origin allowlist.

### BridgeConfiguration

Declares the host application's version, supported capabilities, bootstrap script, and an optional origin allowlist.

```kotlin theme={null}
package com.gbg.gbgbridge.core

public data class BridgeConfiguration(
  val hostVersion: String,
  val capabilities: Map<String, BridgeCapabilityInfo> = emptyMap(),
  val bootstrapScript: String? = null,
  val allowedOrigins: List<String>? = null,
)
```

**Why it exists:** Provides a single configuration object that `BridgeHost` uses to initialise capability state, configure the WebView bootstrap, and (optionally) gate inbound messages by origin.

| Parameter         | Type                                | Default      | Description                                                                                                         |
| ----------------- | ----------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------- |
| `hostVersion`     | `String`                            | —            | A version string for the host app (e.g., `"1.0.0"`). Reported to the web journey via capability queries.            |
| `capabilities`    | `Map<String, BridgeCapabilityInfo>` | `emptyMap()` | A map of capability identifiers to their support status and metadata. Defensively snapshotted at host construction. |
| `bootstrapScript` | `String?`                           | `null`       | Optional custom JavaScript injected on every page load. If `null`, the default script is used.                      |
| `allowedOrigins`  | `List<String>?`                     | `null`       | Optional allowlist of origins (`scheme://host[:port]`) permitted to drive the bridge. `null` disables enforcement.  |

<Note>**Differs from iOS:** `allowedOrigins` is an Android-only addition, and `capabilities` has a default value (`emptyMap()`), so configuration-only hosts can construct `BridgeConfiguration(hostVersion = "1.0.0")` directly.</Note>

#### allowedOrigins

When non-null, inbound `postMessage` calls are checked against the normalised origin of the WebView's main-frame URL. On rejection the message is dropped, `lastError` is set, and `delegate.onError` fires with a `SecurityException`.

The constructor validates the list up front and throws `IllegalArgumentException` for:

* An empty list (which would be a kill-switch with no opt-out — pass `null` to disable enforcement instead).
* Any malformed entry: missing host, non-http(s) scheme, `%` in the host, or a raw internationalised domain label (use punycode A-labels).

Entries and the live URL are both normalised to `scheme://host[:port]` — http/https only, case-insensitive, default ports elided, single trailing host dot stripped — so `"HTTPS://APP.example.com/"`, `"https://app.example.com"`, and `"https://app.example.com:443"` are equivalent.

<Warning>`allowedOrigins` is **not a security boundary**. Android's `addJavascriptInterface` injects into every frame, but only the main-frame URL can be checked — a hostile sub-frame under an allowlisted page can still drive the bridge, and the URL read is subject to time-of-check races. Treat it as defence-in-depth against host misconfiguration, layered on top of navigation-level filtering via `shouldOverrideUrlLoading`. See [Security](/docs/go-v2/developer-integration/sdks/android/security).</Warning>

***

### BridgeCapabilityInfo

Metadata about a single capability.

```kotlin theme={null}
@Serializable
public data class BridgeCapabilityInfo(
  val supported: Boolean,
  val version: String? = null,
  val constraints: Map<String, JsonElement>? = null,
  val permissionState: String? = null,
)
```

**Why it exists:** Allows the host to express not just whether a capability is supported, but also its version and permission state.

| Parameter         | Type                        | Default | Description                                                                                                                                    |
| ----------------- | --------------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `supported`       | `Boolean`                   | —       | Whether the capability is available.                                                                                                           |
| `version`         | `String?`                   | `null`  | The capability implementation version.                                                                                                         |
| `constraints`     | `Map<String, JsonElement>?` | `null`  | Arbitrary key-value constraints. Carried for cross-platform parity but **never emitted** in `capability.query` responses (on either platform). |
| `permissionState` | `String?`                   | `null`  | The native permission status (e.g., `"granted"`, `"denied"`). Included in capability query responses when non-null.                            |

As a Kotlin data class with `val` properties, instances are immutable — derive variants with `.copy(...)`.

***

## Message models

Every value that crosses the bridge is wrapped in a `BridgeMessage` envelope, which carries a typed payload, a correlation ID, and a status. The types in this section describe that envelope and the shapes you'll embed inside it. All of them live in `com.gbg.gbgbridge.models` and are `@Serializable` via kotlinx.serialization; the wire format is identical to iOS.

### BridgeMessage

A structured envelope for all bridge communication.

```kotlin theme={null}
@Serializable
public data class BridgeMessage(
  val version: String,
  val correlationId: String,
  val type: BridgeMessageType,
  val timestamp: Long,
  val payload: BridgePayload,
)
```

| Property        | Type                | Description                                                                                                                            |
| --------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `version`       | `String`            | Protocol version (currently `"1.0"`).                                                                                                  |
| `correlationId` | `String`            | Unique identifier for request-response correlation.                                                                                    |
| `type`          | `BridgeMessageType` | Message type: `REQUEST`, `RESPONSE`, or `EVENT`.                                                                                       |
| `timestamp`     | `Long`              | Milliseconds since the Unix epoch. Decoding is lenient: fractional millisecond values (as iOS may emit) are rounded to a whole `Long`. |
| `payload`       | `BridgePayload`     | The message payload.                                                                                                                   |

<Note>**Differs from iOS:** there is no `Identifiable` conformance or computed `id` property. When rendering messages in a Compose `LazyColumn`, key items by `correlationId` (combined with `timestamp` if you expect to render multiple messages per correlation ID).</Note>

***

### BridgePayload

The payload carried within a `BridgeMessage`.

```kotlin theme={null}
@Serializable
public data class BridgePayload(
  val action: String,
  val data: Map<String, JsonElement>? = null,
  val status: BridgeResponseStatus? = null,
  val error: BridgeErrorPayload? = null,
  val options: BridgeRequestOptions? = null,
)
```

| Property  | Type                        | Default | Description                                                                                         |
| --------- | --------------------------- | ------- | --------------------------------------------------------------------------------------------------- |
| `action`  | `String`                    | —       | The action identifier (e.g., `"camera.document.capture"`, `"capability.query"`).                    |
| `data`    | `Map<String, JsonElement>?` | `null`  | Arbitrary typed JSON data.                                                                          |
| `status`  | `BridgeResponseStatus?`     | `null`  | Response status (only present in response messages).                                                |
| `error`   | `BridgeErrorPayload?`       | `null`  | Error details (only present when the status indicates failure).                                     |
| `options` | `BridgeRequestOptions?`     | `null`  | Per-request options supplied by the web journey (only present on inbound requests that carry them). |

***

### BridgeErrorPayload

Structured error information included in error responses.

```kotlin theme={null}
@Serializable
public data class BridgeErrorPayload(
  val code: String,
  val message: String,
  val recoverable: Boolean,
)
```

**Why it exists:** Provides machine-readable error codes alongside human-readable messages, plus a `recoverable` flag that tells the web journey whether it should retry or fail.

| Property      | Type      | Description                                                                   |
| ------------- | --------- | ----------------------------------------------------------------------------- |
| `code`        | `String`  | A machine-readable error code (e.g., `"CAMERA_DENIED"`, `"HANDLER_FAILURE"`). |
| `message`     | `String`  | A human-readable error description.                                           |
| `recoverable` | `Boolean` | Whether the error is recoverable (e.g., the user can retry).                  |

***

### BridgeRequestOptions

Optional per-request options the web journey can attach to a request payload.

```kotlin theme={null}
@Serializable
public data class BridgeRequestOptions(
  val timeout: Double? = null,
  val fallbackAllowed: Boolean? = null,
)
```

| Property          | Type       | Default | Description                                                                                  |
| ----------------- | ---------- | ------- | -------------------------------------------------------------------------------------------- |
| `timeout`         | `Double?`  | `null`  | A timeout hint supplied by the web journey.                                                  |
| `fallbackAllowed` | `Boolean?` | `null`  | Whether the web journey can fall back to a web-based flow if the native capability declines. |

***

### BridgeMessageType

The type of a bridge message.

```kotlin theme={null}
@Serializable
public enum class BridgeMessageType
```

| Case       | Wire value   | Description                                         |
| ---------- | ------------ | --------------------------------------------------- |
| `REQUEST`  | `"request"`  | A request from the web journey to the native host.  |
| `RESPONSE` | `"response"` | A response from the native host to the web journey. |
| `EVENT`    | `"event"`    | An asynchronous notification (either direction).    |

***

### BridgeResponseStatus

The status of a response message.

```kotlin theme={null}
@Serializable
public enum class BridgeResponseStatus
```

| Case           | Wire value       | When to use                                           |
| -------------- | ---------------- | ----------------------------------------------------- |
| `SUCCESS`      | `"success"`      | The operation completed successfully.                 |
| `ERROR`        | `"error"`        | The operation failed. Include a `BridgeErrorPayload`. |
| `CANCELLED`    | `"cancelled"`    | The user cancelled the operation.                     |
| `UNSUPPORTED`  | `"unsupported"`  | The requested capability is not available.            |
| `ACKNOWLEDGED` | `"acknowledged"` | The request was received; the result will follow.     |

***

### JsonElement

Arbitrary JSON values in payloads are represented with `kotlinx.serialization.json.JsonElement` — the standard kotlinx.serialization JSON tree type — wherever the iOS SDK uses its custom `JSONValue` enum.

<Note>**Differs from iOS:** there is no SDK-defined JSON type. `JsonElement` (with its subtypes `JsonPrimitive`, `JsonObject`, `JsonArray`, and `JsonNull`) comes from the `kotlinx-serialization-json` dependency, so you get the full ecosystem of builders and accessors for free.</Note>

**Example — constructing a payload:**

```kotlin theme={null}
import kotlinx.serialization.json.*

val data: Map<String, JsonElement> = mapOf(
  "name" to JsonPrimitive("John Doe"),
  "age" to JsonPrimitive(30),
  "verified" to JsonPrimitive(true),
  "documents" to buildJsonArray {
    addJsonObject {
      put("type", "passport")
      put("number", "AB123456")
    }
  },
  "metadata" to JsonNull,
)
```

**Example — reading request parameters:**

```kotlin theme={null}
val side = request.payload.data?.get("side")?.jsonPrimitive?.contentOrNull
val attempts = request.payload.data?.get("attempts")?.jsonPrimitive?.intOrNull
```

***

## Interfaces

Where the iOS SDK defines protocols, the Android SDK defines Kotlin interfaces with the same roles: a handler contract for capabilities, a responder for sending results, and a delegate for observing host activity.

### BridgeCapabilityHandler

The interface for objects that handle specific bridge request actions.

```kotlin theme={null}
public interface BridgeCapabilityHandler {
  public val action: String

  @MainThread
  public fun handle(request: BridgeMessage, responder: BridgeResponder)
}
```

**Why it exists:** Defines the contract for capability implementations. Each handler is responsible for a single action and receives a responder to send its result back to the web journey.

#### action

```kotlin theme={null}
public val action: String
```

The action identifier this handler responds to (e.g., `"camera.document.capture"`, `"nfc.read"`). Must be unique per `BridgeHost` — registering a handler with the same action replaces the previous one.

#### handle(request:, responder:)

```kotlin theme={null}
public fun handle(request: BridgeMessage, responder: BridgeResponder)
```

Called on the main thread when a request arrives with a matching action. Perform your native operation and call `responder.respond(...)` when done.

| Parameter   | Type              | Description                                                                         |
| ----------- | ----------------- | ----------------------------------------------------------------------------------- |
| `request`   | `BridgeMessage`   | The incoming request message. Access `request.payload.data` for request parameters. |
| `responder` | `BridgeResponder` | A callback to send the response. Call exactly once.                                 |

<Note>**Differs from iOS:** `handle` is **synchronous**, not `async`. For asynchronous work (camera capture, network calls, pickers), retain the `responder`, kick off the work, and when it completes hop back to the main thread and call `responder.respond(...)`. See [BridgeResponder](#bridgeresponder) for the threading contract and hop patterns.</Note>

**Example:**

```kotlin theme={null}
class DocumentCaptureHandler : BridgeCapabilityHandler {
  override val action = "camera.document.capture"

  override fun handle(request: BridgeMessage, responder: BridgeResponder) {
    // Extract parameters from the request
    val side = request.payload.data?.get("side")?.jsonPrimitive?.contentOrNull

    // Launch asynchronous capture (your implementation), retaining the responder
    captureDocument(
      onSuccess = { imageBytes ->
        Handler(Looper.getMainLooper()).post {
          responder.respond(
            status = BridgeResponseStatus.SUCCESS,
            data = mapOf(
              "imageBase64" to JsonPrimitive(
                Base64.encodeToString(imageBytes, Base64.NO_WRAP)
              ),
            ),
          )
        }
      },
      onFailure = { error ->
        Handler(Looper.getMainLooper()).post {
          responder.respond(
            status = BridgeResponseStatus.ERROR,
            error = BridgeErrorPayload(
              code = "CAPTURE_FAILED",
              message = error.message ?: "Capture failed",
              recoverable = true,
            ),
          )
        }
      },
    )
  }
}
```

***

### BridgeResponder

A callback interface used to send a response from a capability handler back to the web journey.

```kotlin theme={null}
public interface BridgeResponder {
  @MainThread
  public fun respond(
    status: BridgeResponseStatus,
    data: Map<String, JsonElement>? = null,
    error: BridgeErrorPayload? = null,
  )
}
```

**Why it exists:** Decouples the handler from `BridgeHost` internals. Handlers don't need to know about WebViews or JavaScript evaluation — they simply call `respond()`. Implementations are provided by the SDK; you never implement this interface yourself for the built-in dispatch path.

| Parameter | Type                        | Default | Description                |
| --------- | --------------------------- | ------- | -------------------------- |
| `status`  | `BridgeResponseStatus`      | —       | The response status.       |
| `data`    | `Map<String, JsonElement>?` | `null`  | Optional response payload. |
| `error`   | `BridgeErrorPayload?`       | `null`  | Optional error details.    |

**Threading:** Main-thread-only, like the rest of the SDK. Handlers that defer to an asynchronous worker (an executor, OkHttp callback, CameraX callback) must hop back to the main thread before responding:

* **Coroutines:** `withContext(Dispatchers.Main) { responder.respond(...) }`
* **Handler:** `Handler(Looper.getMainLooper()).post { responder.respond(...) }`
* **Activity:** `runOnUiThread { responder.respond(...) }`

<Note>**Differs from iOS:** the iOS responder is safe to call from any thread (it dispatches to the main actor internally). The Android responder throws `IllegalStateException` when called off the main thread — the hop is your responsibility. Call `respond` exactly once per request; subsequent calls silently no-op.</Note>

***

### BridgeHostDelegate

An observer interface for monitoring bridge activity. All methods have default no-op implementations, so you only override what you need.

```kotlin theme={null}
public interface BridgeHostDelegate {
  public fun onMessage(host: BridgeHost, message: BridgeMessage) {}

  public fun onMessageSent(host: BridgeHost, message: BridgeMessage) {}

  public fun onUnhandledRequest(host: BridgeHost, request: BridgeMessage) {}

  public fun onError(host: BridgeHost, error: Throwable) {}
}
```

**Why it exists:** Provides a way to observe all bridge messages and react to unhandled requests and errors without registering a handler. Useful for logging, analytics, or building custom request handling.

<Note>**Differs from iOS:** Kotlin uses distinct method names rather than Swift argument labels — `onMessage` corresponds to `bridgeHost(_:didReceive:)` and `onUnhandledRequest` to `bridgeHost(_:unhandledRequest:)`. `onMessageSent` and `onError` are Android-only additions. Remember that `BridgeHost.delegate` is `WeakReference`-backed: keep your own strong reference to the delegate.</Note>

#### onMessage(host:, message:)

Called for every message received from the web journey, regardless of type. One-way journey events (e.g. `journey.completed`) are observed here — events do not go through the request dispatch path.

#### onMessageSent(host:, message:)

Called for every outbound envelope, **before** transport. This records intent, not delivery — it also fires for messages that are subsequently dropped because no WebView is attached, or that fail to encode. Android-only.

#### onUnhandledRequest(host:, request:)

Called when a request arrives with no registered handler. The request is also added to `host.pendingRequests`; respond later via the lookup `respond` overload.

#### onError(host:, error:)

The Throwable-level error channel, Android-only. Fires for inbound decode failures, handler exceptions, outbound encode failures, and origin-gate rejections (`SecurityException`). Pairs with the string-valued `lastError` property for hosts that want full exception objects.

***

## Built-in handlers

The SDK auto-registers one handler on every host so capability negotiation works out of the box.

### CapabilityQueryHandler

A built-in handler that responds to `"capability.query"` requests with the host's declared capabilities.

```kotlin theme={null}
public class CapabilityQueryHandler(
  private val capabilitiesProvider: () -> Map<String, BridgeCapabilityInfo>,
  private val hostVersion: String,
) : BridgeCapabilityHandler
```

**Why it exists:** Capability negotiation is fundamental to the bridge protocol. This handler is automatically registered by `BridgeHost` so the web journey can always discover available capabilities.

**Action:** `"capability.query"`

| Parameter              | Type                                      | Description                                                             |
| ---------------------- | ----------------------------------------- | ----------------------------------------------------------------------- |
| `capabilitiesProvider` | `() -> Map<String, BridgeCapabilityInfo>` | A lambda that returns the current capability map. Called on each query. |
| `hostVersion`          | `String`                                  | The host version string to include in the response.                     |

<Note>**Differs from iOS:** there is a single constructor taking a rich `BridgeCapabilityInfo` provider — no separate "simple" boolean-map initialiser. The class is public (iOS keeps its equivalent internal) so hosts that call `unregister("capability.query")` can register a replacement instance with their own provider.</Note>

#### Response format

When the web journey sends a `capability.query` request, the response `data` contains:

```json theme={null}
{
  "environment": "android",
  "hostVersion": "1.0.0",
  "capabilities": {
    "camera.document": {
      "supported": true,
      "version": "1.0",
      "permissionState": "granted"
    },
    "camera.selfie": {
      "supported": false,
      "version": null
    }
  }
}
```

| Field                              | Type      | Description                                                        |
| ---------------------------------- | --------- | ------------------------------------------------------------------ |
| `environment`                      | `String`  | Always `"android"` for this SDK.                                   |
| `hostVersion`                      | `String`  | The host version from configuration.                               |
| `capabilities`                     | `Object`  | Map of capability ID → `{ supported, version, permissionState? }`. |
| `capabilities[id].supported`       | `Boolean` | Always present.                                                    |
| `capabilities[id].version`         | `String?` | Always present; JSON `null` when unset.                            |
| `capabilities[id].permissionState` | `String?` | Present only when the capability provides permission metadata.     |

`constraints` is never emitted, on either platform.
