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

# Messaging

> How messages flow between your Android app and the web journey.

This guide explains how to send events to the web journey, handle incoming requests, and build request-response flows.

## Message Types Overview

GBGBridge uses three message types:

| Type         | Direction        | Purpose                                                            |
| ------------ | ---------------- | ------------------------------------------------------------------ |
| **Request**  | Web → Native     | The web journey asks the host to do something. Expects a response. |
| **Response** | Native → Web     | The host returns the result of a request.                          |
| **Event**    | Either direction | An asynchronous notification. No response expected.                |

All messages share the same envelope structure (`BridgeMessage`) with a `correlationId` for pairing requests and responses. Message data values are `kotlinx.serialization.json.JsonElement` — build payloads with `JsonPrimitive(...)` or `buildJsonObject { }`, and read values with extensions like `jsonPrimitive.contentOrNull`.

<Warning>
  `BridgeHost` is main-thread-only. Every state-mutating call (`sendEvent`, `respond`, `register`, and the rest) asserts the main thread at runtime and throws `IllegalStateException` off-main. Inbound messages from the web journey are posted to the main looper before any handler or delegate runs. If you do work on a background thread, hop back before touching the host — `withContext(Dispatchers.Main)`, `Handler(Looper.getMainLooper()).post { }`, or `runOnUiThread { }`.
</Warning>

## Sending Events to the Web Journey

Events are fire-and-forget messages from the native host to the web journey. Use them to notify the web journey of state changes, user actions, or lifecycle transitions.

```kotlin theme={null}
// Send a simple event
host.sendEvent("host.ready", data = mapOf(
  "timestamp" to JsonPrimitive(System.currentTimeMillis())
))

// Send an event with structured data
host.sendEvent("user.action", data = mapOf(
  "action" to JsonPrimitive("tapped_help"),
  "screen" to JsonPrimitive("document_capture")
))

// Nested objects via buildJsonObject
host.sendEvent("host.deviceInfo", data = mapOf(
  "device" to buildJsonObject {
    put("model", Build.MODEL)
    put("osVersion", Build.VERSION.RELEASE)
  }
))
```

`sendEvent` returns `Unit` — there is no delivery confirmation. The SDK generates a correlationId of the form `android-event-{uuid}` for each event (the iOS SDK uses `ios-event-{uuid}`).

### Common Event Patterns

| Event Action       | When to Send                        | Example Data      |
| ------------------ | ----------------------------------- | ----------------- |
| `host.ready`       | After host initialization completes | `{ timestamp }`   |
| `host.background`  | App enters background               | `{}`              |
| `host.foreground`  | App returns to foreground           | `{}`              |
| `journey.cancel`   | User taps a cancel/back button      | `{ reason }`      |
| `host.orientation` | Device orientation changes          | `{ orientation }` |

## Handling Incoming Requests

When the web journey sends a request, GBGBridge routes it to a registered handler or adds it to `pendingRequests`.

### Using Typed Capability Slots (Recommended)

For capture operations, use the typed slots on `BridgeHost`. Setting a handler declares support and routes requests automatically. The handler is a `suspend` lambda that runs on `Dispatchers.Main`:

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

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

Complete the request from your camera UI callback:

```kotlin theme={null}
// Success
host.documentCapture.complete(CaptureResult.Document(
  imageData = imageBytes, width = 1920, height = 1080
))

// Cancellation
host.documentCapture.complete(CaptureResult.Cancelled(reason = "User dismissed"))

// Failure
host.documentCapture.complete(CaptureResult.Failed(
  code = "CAMERA_DENIED", message = "Permission denied", recoverable = true
))
```

The SDK encodes `CaptureResult` into the bridge protocol format automatically. See the [Capability Handling Guide](/docs/go-v2/developer-integration/sdks/android/capability-handling) for the full Compose integration pattern.

### Using Custom Capabilities or BridgeCapabilityHandler

For other capabilities, use `registerCustomCapability()` or implement `BridgeCapabilityHandler`:

```kotlin theme={null}
// Custom capability (lambda-based)
host.registerCustomCapability("device.info") { request, responder ->
  responder.respond(
    status = BridgeResponseStatus.SUCCESS,
    data = mapOf("model" to JsonPrimitive(Build.MODEL))
  )
}

// BridgeCapabilityHandler interface (class-based)
host.register(handler = DeviceInfoHandler())
```

Unlike iOS, where `handle` is `async`, the Kotlin `BridgeCapabilityHandler.handle(request, responder)` signature is synchronous. For asynchronous work, retain the responder, do the work off the main thread, then hop back to the main thread and respond:

```kotlin theme={null}
class DeviceInfoHandler(private val scope: CoroutineScope) : BridgeCapabilityHandler {
  override val action = "device.info"

  override fun handle(request: BridgeMessage, responder: BridgeResponder) {
    scope.launch(Dispatchers.IO) {
      val model = loadDeviceModel()
      withContext(Dispatchers.Main) {
        responder.respond(
          status = BridgeResponseStatus.SUCCESS,
          data = mapOf("model" to JsonPrimitive(model))
        )
      }
    }
  }
}
```

Call `responder.respond(...)` exactly once — subsequent calls on the same responder are silently ignored.

### Handling Requests via the Delegate

For requests that don't map to a single handler — or when you want centralized request handling — use `BridgeHostDelegate`. All four delegate methods have default no-op implementations, so override only what you need. `onMessageSent` and `onError` are Android-only additions with no iOS equivalent.

```kotlin expandable theme={null}
class JourneyCoordinator : BridgeHostDelegate {
  override fun onUnhandledRequest(host: BridgeHost, request: BridgeMessage) {
    when (request.payload.action) {
      "journey.complete" -> {
        val result = request.payload.data?.get("result")?.jsonPrimitive?.contentOrNull
        handleJourneyComplete(result)

        host.respond(
          to = request.correlationId,
          status = BridgeResponseStatus.ACKNOWLEDGED
        )
      }

      else -> host.respond(
        to = request.correlationId,
        status = BridgeResponseStatus.UNSUPPORTED,
        error = BridgeErrorPayload(
          code = "UNSUPPORTED_ACTION",
          message = "Action '${request.payload.action}' is not supported",
          recoverable = false
        )
      )
    }
  }

  override fun onMessage(host: BridgeHost, message: BridgeMessage) {
    // Log all inbound messages for debugging
    Log.d("Bridge", "${message.type}: ${message.payload.action}")
  }

  private fun handleJourneyComplete(result: String?) {
    // Navigate to results screen, etc.
  }
}
```

Set the delegate on your host:

```kotlin theme={null}
val coordinator = JourneyCoordinator() // keep a strong reference
host.delegate = coordinator
```

<Warning>
  The `delegate` property is backed by a `WeakReference` — the host does not keep your delegate alive. Assigning an inline instance (`host.delegate = JourneyCoordinator()`) with no other strong reference means callbacks silently stop firing once the delegate is garbage collected. Hold the delegate in a property of an object with a matching lifetime (Activity, Fragment, ViewModel, or a `remember`-ed controller in Compose).
</Warning>

### Responding to Pending Requests Manually

If a request arrives with no registered handler, it is stored in `pendingRequests` (and `onUnhandledRequest` fires). You can respond to it later:

```kotlin theme={null}
// In a Compose screen, with pendingRequests copied into state
// from a delegate callback
pendingRequests.forEach { request ->
  key(request.correlationId) {
    Row {
      Text(request.payload.action)
      Button(onClick = {
        host.respond(
          to = request.correlationId,
          status = BridgeResponseStatus.SUCCESS,
          data = mapOf("approved" to JsonPrimitive(true))
        )
      }) { Text("Approve") }
      Button(onClick = {
        host.respond(
          to = request.correlationId,
          status = BridgeResponseStatus.CANCELLED
        )
      }) { Text("Deny") }
    }
  }
}
```

`pendingRequests` is an immutable snapshot taken on each read — it is not observable. Re-read it after delegate callbacks fire and copy it into your own UI state. `BridgeMessage` has no `id` property on Android; key Compose lists by `correlationId`.

### Choosing a respond Overload

`BridgeHost` has two `respond` overloads, and they behave differently. Both return `Unit`.

The **lookup overload** — `respond(to, status, data, error)` — finds the matching request in `pendingRequests` by correlationId, removes it, and sends the response using the action recorded on the original request. If no pending request matches the correlationId, the call silently no-ops. Use it for requests that landed on the unhandled path.

The **explicit-action overload** — `respond(to, action, status, data, error)` — sends a response without requiring a pending entry; you supply the action yourself. It deduplicates by correlationId: a second call with the same correlationId silently no-ops. If the send fails (encode error, or no WebView attached), the dedupe entry is rolled back so a retry with the same correlationId works once the underlying problem is fixed.

<Warning>
  There is a retry asymmetry between the overloads. The lookup overload consumes the pending entry *before* sending — if that send then fails, retrying via the lookup overload silently no-ops because the entry is gone. Retry with the explicit-action overload instead, recovering the action from the original `BridgeMessage` (for example, one you captured in `onUnhandledRequest`).
</Warning>

## Response Patterns

When responding to a request, you supply a status, optional data, and an optional error payload. The patterns below cover the three response shapes you'll use most: success-with-data, error, and cancellation.

### Success with Data

```kotlin theme={null}
responder.respond(
  status = BridgeResponseStatus.SUCCESS,
  data = mapOf(
    "imageBase64" to JsonPrimitive("..."),
    "width" to JsonPrimitive(1920),
    "height" to JsonPrimitive(1080)
  )
)
```

### Error with Details

```kotlin theme={null}
responder.respond(
  status = BridgeResponseStatus.ERROR,
  error = BridgeErrorPayload(
    code = "PERMISSION_DENIED",
    message = "Camera permission was denied by the user",
    recoverable = true  // User can grant permission in Settings
  )
)
```

### User Cancellation

```kotlin theme={null}
responder.respond(status = BridgeResponseStatus.CANCELLED)
```

### Unsupported Action

```kotlin theme={null}
responder.respond(
  status = BridgeResponseStatus.UNSUPPORTED,
  error = BridgeErrorPayload(
    code = "NOT_AVAILABLE",
    message = "This capability is not available on this device",
    recoverable = false
  )
)
```

### Acknowledged (Async Processing)

Use `ACKNOWLEDGED` when the operation will take time and you want to inform the web journey that the request was received.

```kotlin theme={null}
// First, acknowledge receipt
responder.respond(
  status = BridgeResponseStatus.ACKNOWLEDGED,
  data = mapOf("estimatedDuration" to JsonPrimitive(5000))
)

// Later, send the actual result as an event
host.sendEvent("camera.capture.complete", data = mapOf(
  "imageBase64" to JsonPrimitive("..."),
  "correlationId" to JsonPrimitive(request.correlationId)
))
```

## Observing All Messages

The Android SDK has no Combine-style publishers. Observe traffic through the delegate callbacks, and read the `receivedMessages` / `pendingRequests` snapshot properties when you need current state:

```kotlin theme={null}
class MessageLogger : BridgeHostDelegate {
  override fun onMessage(host: BridgeHost, message: BridgeMessage) {
    // Every inbound message
    Log.d("Bridge", "Total messages: ${host.receivedMessages.size}")
    if (message.payload.action == "journey.progress") {
      // Update progress UI
    }
  }

  override fun onMessageSent(host: BridgeHost, message: BridgeMessage) {
    // Every outbound message, fired before transport (Android-only)
    Log.d("Bridge", "Outbound: ${message.payload.action}")
  }
}
```

`receivedMessages` holds every inbound message, capped at 200 entries (`BridgeHost.MAX_RECEIVED_MESSAGES`) — older entries are evicted from the head. Both properties return an immutable copy on each read, so for Compose, copy them into your own state from the delegate callbacks rather than reading them directly in composition.

<Note>
  `onMessageSent` fires for every outbound envelope the SDK was asked to send — including envelopes that are then dropped at transport (no WebView attached) or fail to encode. Treat it as an intent trace, not delivery confirmation.
</Note>

## Error Handling

Bridge messaging surfaces errors through two channels: `lastError`, a read-only string property on the host (clear it with `clearError()`; hosts cannot write it directly, unlike iOS), and `BridgeHostDelegate.onError`, an Android-only `Throwable` channel that fires for decode failures, handler exceptions, encode failures, and origin-gate rejections.

```kotlin theme={null}
override fun onError(host: BridgeHost, error: Throwable) {
  Log.e("Bridge", "Bridge error", error)
}
```

### Encoding/Decoding Errors

If GBGBridge cannot decode an incoming message, `lastError` is set with a description, `onError` fires, and the message is not added to `receivedMessages`. If an outbound message fails to encode, `lastError` is set, `onError` fires, and the message is dropped — the explicit-action `respond` overload rolls back its dedupe entry in this case so a retry can succeed.

### Sending While Detached

If you call `sendEvent` or `respond` before attaching a WebView (or after `detach()`), the message is silently dropped at transport. `onMessageSent` still fires — it records intent, not delivery — and **no `lastError` is recorded**. This deliberately diverges from iOS, which sets `lastError = "WebView not attached"`. Since `respond` and `sendEvent` return `Unit`, there is no per-call failure signal; make sure the host is attached before sending.

### Handler Exceptions

Exceptions thrown by capability handlers are caught by the SDK and routed to `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 journey. A handler that responded successfully and *then* threw is reported via `onError` only — the request itself still succeeded.

### JavaScript Delivery

Outbound delivery into the page is fire-and-forget: the injected script no-ops when `window.GBGBridge.receive` is not defined (for example, after the page has navigated away), and no error is reported back to the host. This differs from iOS, where WebKit evaluation failures set `lastError`.

## Next Steps

* [Capability Handling Guide](/docs/go-v2/developer-integration/sdks/android/capability-handling) — Build handlers for camera capture and custom capabilities
* [Embedding Guide](/docs/go-v2/developer-integration/sdks/android/embedding) — Compose and View-system integration patterns
* [API Reference](/docs/go-v2/developer-integration/sdks/android/api-reference) — Detailed method signatures and parameters
