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

# Concepts

> Architecture, message protocol, capability model, and design rationale behind the GBGBridge Web SDK.

This document explains the architecture and design principles behind the GBGBridge Web SDK. Understanding these concepts will help you integrate effectively and make informed decisions about how to structure your host page.

## Overview

GBGBridge solves a fundamental problem: web-based identity verification journeys, when embedded as an iframe inside another web page, are subject to browser-level origin isolation that prevents the journey from accessing host-page state, calling host functions, or triggering host UI directly. Rather than building separate host-page logic for every journey variation, GBGBridge provides a structured `postMessage` protocol so a single journey can run anywhere — inside an iOS WebView, an Android WebView, or an iframe on a web host — and request host capabilities through the same interface.

The Web Bridge SDK is the **host-page** half of that protocol. The other half — the iframe-side SDK that runs inside the journey iframe — is documented separately under the [TypeScript Bridge SDK](/docs/go-v2/developer-integration/sdks/typescript-sdk).

## Key Terminology

| Term                 | Definition                                                                                                                                                                                                                               |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Journey**          | A web-based identity verification flow (e.g., document capture + face match). The journey runs as HTML/JavaScript loaded inside an `<iframe>`.                                                                                           |
| **Host page**        | The web page that embeds the journey iframe. The host provides capabilities the journey requests.                                                                                                                                        |
| **Bridge**           | The communication layer between the journey and the host page. Comprises the iframe-side `NativeBridge` (in the journey) and `BridgeHost` (in the host page).                                                                            |
| **Capability**       | A feature the host can provide (e.g., `camera.document`, `camera.selfie`). Declared via the `capabilities` prop on `BridgeHostProvider` or via `registerCustomCapability(...)` on the underlying host.                                   |
| **Action**           | The string identifier for a specific request or event (e.g., `"camera.document.capture"`, `"capability.query"`). Distinct from capability ID — see [Declaring vs. Registering](#declaring-vs-registering-capabilities).                  |
| **Handler**          | A `BridgeCapabilityHandler` — an object with a `handle(request, responder)` method that fulfils requests for a specific action.                                                                                                          |
| **Responder**        | A `BridgeResponder` instance passed to handlers; used to send a response back to the journey via `success`, `error`, `cancelled`, `unsupported`, or `acknowledge`.                                                                       |
| **Origin allowlist** | The list of origins the host trusts to source bridge messages. Inbound `message` events whose `event.origin` is not in this list are silently dropped.                                                                                   |
| **Permission state** | Metadata about a capability's current permission status (e.g., `granted`, `denied`, `prompt`). Reported in capability query responses when set.                                                                                          |
| **Message**          | A structured JSON envelope exchanged over `postMessage`. Every message has a `type` (`request`, `response`, or `event`), a `correlationId`, a `timestamp`, a protocol `version`, and a `payload`.                                        |
| **Correlation ID**   | A unique string that links a request to its response. Host-emitted events use the format `web-event-{uuid}`.                                                                                                                             |
| **Bootstrap script** | *Not present on web.* iOS and Android inject a JavaScript snippet into the WebView at document start to expose `window.GBGBridge`. On web, the journey iframe loads its own SDK; the host page does not inject anything into the iframe. |

## Architecture

The Web SDK has two layered packages: a framework-agnostic core (`@gbg/go-bridge-web`) and a React adapter (`@gbg/go-bridge-web-react`) that wraps the core in a Provider and hooks.

### Component Diagram

```mermaid theme={null}
graph TB
    subgraph Host["Host Page (Your Web App)"]
        BHP[BridgeHostProvider<br/>React adapter — optional]
        BH[BridgeHost<br/>Message Router]
        HIC[HostIframeChannel<br/>postMessage transport]
        HM[Capability Handlers]
        CQH[CapabilityQueryHandler<br/>Built-in]
        DEL[BridgeHostDelegate<br/>Observer]
    end

    subgraph IFrame["iframe element"]
        IF[HTMLIFrameElement]
    end

    subgraph Web["Web Journey (iframe-side SDK)"]
        NB[NativeBridge<br/>iframe-side]
        JS[Journey Code]
    end

    BHP -.->|instantiates| BH
    HM --> BH
    CQH --> BH
    DEL -.-> BH
    BH --> HIC
    HIC --> IF
    IF -- "iframe.contentWindow.postMessage" --> NB
    NB -- "window.postMessage<br/>(origin-validated)" --> HIC
    JS --> NB
    NB --> JS
```

`BridgeHostProvider` is a thin React adapter that owns the `BridgeHost` lifecycle. Non-React hosts construct `new BridgeHost(...)` directly. The transport details live in `HostIframeChannel`, which the host owns internally.

### Layers

1. **Adapter Layer** — `BridgeHostProvider` (React) holds the underlying `BridgeHost` in component state, snapshots config props on attach, and tears down on unmount. Non-React hosts skip this layer.
2. **Routing Layer** — `BridgeHost` receives validated messages from the channel, appends them to observable buffers, fans out to the delegate, and routes `request`-type messages to registered handlers.
3. **Handler Layer** — `BridgeCapabilityHandler` is an interface with a single `handle(request, responder)` method. The built-in `CapabilityQueryHandler` services `capability.query` automatically; integrators register additional handlers via `useBridgeCapability(...)` (React) or `host.registerHandler(...)` (direct).
4. **Transport Layer** — `HostIframeChannel` owns the single `window` message listener, performs origin and envelope validation, and uses `iframe.contentWindow.postMessage` for outbound sends. Origin checks are enforced on both inbound and outbound traffic.
5. **Observation Layer** — `BridgeHostDelegate` exposes four optional callbacks — `onMessage`, `onUnhandledRequest`, `onMessageSent`, `onError` — for full visibility into the message flow.

## Message Protocol

All communication uses structured JSON envelopes. The protocol version is `1.0`, and the wire format is identical to the iOS and Android SDKs — a web journey that speaks the bridge protocol works unchanged on all three platforms.

### Message Envelope

```json theme={null}
{
  "version": "1.0",
  "correlationId": "uuid-string",
  "type": "request | response | event",
  "timestamp": 1700000000000,
  "payload": {
    "action": "camera.document.capture",
    "data": { },
    "status": "success | error | cancelled | unsupported | acknowledged",
    "error": {
      "code": "CAMERA_DENIED",
      "message": "User denied camera access",
      "recoverable": true
    }
  }
}
```

`timestamp` is epoch milliseconds. `payload.action` is required on all three message types — envelopes without a non-empty string action are rejected at the transport layer.

The wire protocol version is exposed as `BridgeHost.PROTOCOL_VERSION` for telemetry. On version mismatch, the host logs a warning and continues processing rather than failing — this lets minor protocol additions roll out without breaking older integrations.

### Message Types

| Type       | Direction        | Purpose                                                     |
| ---------- | ---------------- | ----------------------------------------------------------- |
| `request`  | Iframe → Host    | The journey asks the host to perform an action.             |
| `response` | Host → Iframe    | The host sends the result of a request back to the journey. |
| `event`    | Either direction | An asynchronous notification with no expected response.     |

### Message Flow

```mermaid theme={null}
sequenceDiagram
    participant Web as Web Journey (iframe)
    participant Channel as HostIframeChannel
    participant Bridge as BridgeHost
    participant Handler as CapabilityHandler

    Web->>Channel: postMessage (capability.query)
    Channel->>Channel: validate source + origin + envelope
    Channel->>Bridge: deliver BridgeMessage
    Bridge->>Handler: route to CapabilityQueryHandler
    Handler->>Bridge: responder.success(capabilities)
    Bridge->>Channel: postMessage (capability.query response)
    Channel->>Web: receive

    Web->>Channel: postMessage (camera.document.capture)
    Channel->>Bridge: deliver
    Bridge->>Handler: route to your handler
    Note over Handler: Run capture UI,<br/>resolve image data
    Handler->>Bridge: responder.success(image)
    Bridge->>Web: response (camera.document.capture)

    Bridge->>Web: event (host.theme.update)
    Note over Web: Update UI based on event
```

### Correlation IDs

Every request has a `correlationId`. When the host responds, it includes the same `correlationId` so the journey can match the response to its original request. Host-emitted events generate their own unique correlation IDs in the format `web-event-{uuid}` (the SDK uses `crypto.randomUUID()` where available, with a timestamp+random fallback for older environments).

### Response Statuses

| Status         | Meaning                                                                                                                                                     |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `success`      | The request completed successfully. Check `data` for the result.                                                                                            |
| `error`        | The request failed. Check `error` for details.                                                                                                              |
| `cancelled`    | The user cancelled the operation (e.g. dismissed the capture sheet).                                                                                        |
| `unsupported`  | The requested capability is not available on this host.                                                                                                     |
| `acknowledged` | The request was received and is being processed asynchronously. Interim; a terminal status (`success` / `error` / `cancelled` / `unsupported`) must follow. |

`BridgeResponder` enforces at-most-once delivery on terminal statuses. After a terminal status is sent, subsequent calls on the same responder are silently no-ops. `acknowledge()` is the one exception: it may be called once before any terminal status, and the responder remains usable for the terminal call.

### Error Codes

The protocol defines a small set of well-known error codes, exposed as the `BridgeErrorCode` enum:

| Code                | Meaning                                                        |
| ------------------- | -------------------------------------------------------------- |
| `UNSUPPORTED`       | The host does not support this action.                         |
| `PERMISSION_DENIED` | The user or browser denied permission required for the action. |
| `CANCELLED`         | The user cancelled the operation.                              |
| `TIMEOUT`           | The request timed out before completing.                       |
| `BUSY`              | The host is already handling another request for this action.  |
| `INVALID_REQUEST`   | The request payload was malformed or missing required fields.  |
| `INTERNAL_ERROR`    | An unexpected error occurred in the handler.                   |
| `VERSION_MISMATCH`  | Protocol version mismatch between host and journey.            |

Handlers that throw an uncaught exception trigger an automatic `error` response with code `HANDLER_FAILURE` and `recoverable: false`, so the journey is never left waiting on a request that the host silently dropped. The error is also routed to `delegate.onError`.

## Request Routing

When a message arrives from the journey iframe:

1. The `HostIframeChannel`'s `window` `message` listener fires.
2. **Source check** — Events whose `event.source` is not the iframe's `contentWindow` are dropped silently. This prevents cross-window leakage from other iframes on the page.
3. **Origin check** — Events whose `event.origin` is not in `allowedOrigins` (after normalization) are dropped silently.
4. **Envelope validation** — Events with malformed envelopes (missing `version`, `correlationId`, `type`, `timestamp`, `payload.action`, or invalid `type`) are dropped silently.
5. The validated `BridgeMessage` is handed to `BridgeHost.handleIncomingMessage`.
6. The message is appended to `receivedMessages` (an observable buffer capped at the most recent 200 messages).
7. `delegate.onMessage(host, message)` fires.
8. If `message.type === 'request'`:
   * If a handler is registered for `payload.action`, `handler.handle(message, responder)` is called.
   * If no handler is registered, the message is pushed to `pendingRequests` (capped at 50; oldest dropped on overflow) and `delegate.onUnhandledRequest(host, message)` fires. You can respond later via `host.respond({ correlationId, status, ... })`.
9. If `message.type === 'response'` or `'event'`, no routing occurs — the message is just observed.

```mermaid theme={null}
flowchart TD
    A[window message event] --> B{source = iframe.contentWindow?}
    B -->|No| Z[Drop silently]
    B -->|Yes| C{origin in allowedOrigins?}
    C -->|No| Z
    C -->|Yes| D{Valid envelope?}
    D -->|No| Z
    D -->|Yes| E[Append to receivedMessages]
    E --> F[Notify delegate: onMessage]
    F --> G{Message type?}
    G -->|response/event| H[Done — stored and observed]
    G -->|request| I{Handler registered?}
    I -->|Yes| J[Call handler.handle]
    J --> K[Handler calls responder]
    K --> L[Response sent via channel]
    I -->|No| M[Add to pendingRequests]
    M --> N[Notify delegate: onUnhandledRequest]
```

Handler exceptions are caught: the error is routed to `delegate.onError`, and if the handler had not yet responded, an `error` response with code `HANDLER_FAILURE` is dispatched automatically so the journey is never left waiting.

## Declaring vs. Registering Capabilities

A platform divergence worth understanding upfront: on iOS and Android, the Bridge SDK exposes **typed capability slots** — first-class properties on `BridgeHost` (e.g. `host.documentCapture`) where setting a handler simultaneously declares the capability as supported. The web SDK does not have typed slots. Instead, **declaration and registration are two separate calls**:

| Concern            | iOS / Android                                               | Web                                                                                        |
| ------------------ | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| Declare capability | Set handler on typed slot (auto-declares)                   | Pass entry in `capabilities` prop OR call `registerCustomCapability(id, version, handler)` |
| Register handler   | Set handler on typed slot OR `register(handler)` for custom | Call `useBridgeCapability(action, handler)` OR `host.registerHandler(action, handler)`     |
| One call does both | Yes (typed slot handler assignment)                         | Only `registerCustomCapability(id, version, handler)`                                      |

In the typical React pattern:

```tsx theme={null}
<BridgeHostProvider
  capabilities={{ "camera.document": { supported: true, version: "1.0" } }}  // declare
>
  <DocumentCaptureHandler />  // register inside this component
</BridgeHostProvider>
```

If you only register the handler (with no `capabilities` entry), the journey's `capability.query` response will not list `camera.document` as supported. The journey will fall back to its alternative flow even though your handler is sitting ready. **This is the most common integration mistake** — there is a dedicated entry in [Getting Started → Common Pitfalls](/docs/go-v2/developer-integration/sdks/web/getting-started#capability-query-returns-the-wrong-shape).

For dynamic capability additions at runtime (e.g. after a permission grant), use `host.registerCustomCapability(id, version, handler)` — it declares and registers in one call.

### Capability ID vs. Action ID

The protocol uses two related strings:

* **Capability ID** (e.g. `camera.document`) — names the feature. Appears in the `capabilities` map and `capability.query` responses. Listed as constants in `CAPABILITY_IDS`.
* **Action ID** (e.g. `camera.document.capture`) — names the specific operation. Appears in `useBridgeCapability(...)`, `registerHandler(...)`, and inbound `payload.action`.

Action IDs typically contain the capability ID plus a verb suffix. The journey sends a request with `payload.action = "camera.document.capture"`; the host has previously registered a handler for that action and declared support for the underlying `camera.document` capability.

## Capability Negotiation

Before invoking a capability, the journey sends a `capability.query` request. The host's built-in `CapabilityQueryHandler` responds automatically with the effective capability map — your declared `capabilities` plus any added via `registerCustomCapability`.

### Query Response Format

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

`supported` and `version` are always emitted (`version` is `null` when unset). `permissionState` is included only when you set it in the `CapabilityInfo` entry. The `environment` field is the literal string `"web"` for this SDK; iOS reports `"ios"`, Android reports `"android"`.

### Why declare?

The journey uses this response to:

* **Adapt its UI** — show or hide steps based on what the host can fulfil.
* **Prevent errors** — skip requests for unsupported capabilities and fall straight to alternatives.
* **Check permissions** — surface permission state before triggering a request, avoiding immediate denial.
* **Degrade gracefully** — use an in-iframe fallback (e.g. `getUserMedia` in the iframe itself) when the host doesn't expose a capability.

## Transport Mechanism

The Web SDK uses the browser's native `postMessage` channel in both directions, with origin validation enforced on both sides.

### Iframe → Host (Incoming)

The journey iframe sends:

```javascript theme={null}
window.postMessage(messageObject, "*");
```

The host page's `window` `message` listener receives the event. `HostIframeChannel` filters it: `event.source` must equal the host's `iframe.contentWindow`, `event.origin` must be in `allowedOrigins`, and the envelope must validate. Surviving events are decoded and routed to `BridgeHost`.

There is **no script injection** in the iframe — the host page does not inject any JavaScript into the journey iframe. Cross-origin restrictions would prevent it anyway. The journey loads its own iframe-side SDK independently as part of its bundle.

### Host → Iframe (Outgoing)

The host sends:

```javascript theme={null}
iframe.contentWindow.postMessage(messageObject, targetOrigin);
```

`HostIframeChannel` resolves `targetOrigin` from `iframe.src` at send time (so a late `iframe.src` assignment is picked up) and validates it against `allowedOrigins` — outbound messages to a non-allowlisted origin are dropped with a `console.warn`. This guards against a repointed `iframe.src` leaking payloads to a non-trusted host.

### Why origin validation on both sides?

Same-origin policy doesn't prevent a page from posting messages to an iframe whose `contentWindow` it holds a reference to — it only blocks direct DOM access. Without explicit origin filtering, a malicious script on the host page could redirect the iframe to a different origin (via `iframe.src = ...`) and start receiving the host's outbound messages. Two-sided origin validation closes that hole.

### Comparison with iOS and Android

| Aspect           | iOS                                                             | Android                                                                 | Web                                                   |
| ---------------- | --------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------- |
| Inbound channel  | `webkit.messageHandlers.gbgBridge.postMessage(obj)`             | `window.GBGBridge.postMessage(jsonString)` via `addJavascriptInterface` | `window.postMessage(obj)`                             |
| Outbound channel | `WKWebView.evaluateJavaScript("window.GBGBridge.receive(...)")` | `WebView.evaluateJavascript("window.GBGBridge.receive(...)")`           | `iframe.contentWindow.postMessage(obj, targetOrigin)` |
| Bootstrap script | `WKUserScript` at document start (guaranteed before page JS)    | `evaluateJavascript` in `onPageStarted` (best-effort)                   | None — journey loads its own SDK                      |
| Origin allowlist | n/a (WebView source restriction instead)                        | n/a                                                                     | Required, non-empty                                   |

## React Lifecycle

`BridgeHostProvider` ties the underlying `BridgeHost` instance to React component state. Three rules govern this binding:

1. **Build on iframe attach.** The provider holds off until `iframe` is non-null. When the iframe element appears (via the `ref={setIframe}` callback), the provider instantiates `BridgeHost` and stores it in state, which then propagates through `BridgeHostContext` to child hooks.
2. **Config snapshot at attach time.** `allowedOrigins`, `hostVersion`, and `capabilities` are read into a snapshot when the host is built. Subsequent renders that change these props do **not** rebuild the host. To apply changes, force a Provider remount (`key={...}` rotation) or call methods on the host instance directly (e.g. `registerCustomCapability`).
3. **Detach on unmount.** When the provider unmounts (or the iframe transitions back to null), the effect cleanup calls `host.detach()` and clears the host from state. The internal `window` `message` listener is removed at this point, so there is no leak.

For non-React hosts using `new BridgeHost(...)` directly, you own the lifecycle. Call `host.detach()` when the iframe is no longer in use to remove the message listener and free any pending request buffers.

### Threading model

JavaScript is single-threaded — there is no main-thread / background-thread distinction to manage. Handlers may be `async` and the responder's terminal methods can be called from any continuation. The protocol's at-most-once enforcement is purely state-based, not thread-based.

## Platform divergences from iOS / Android

The Web SDK speaks the same wire protocol as iOS and Android, but a few implementation details differ because the browser model isn't a native WebView. The table lists the divergences integrators porting host code from iOS or Android are most likely to hit.

| Concern                    | iOS              | Android     | Web                     | Why                                                                                                                                                                                                                                                     |
| -------------------------- | ---------------- | ----------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Typed capability slots     | ✓                | ✓           | ✗                       | Native hosts hand off camera control to platform SDKs. On web, the iframe-side journey already has `getUserMedia` — there's no "other side" to delegate to. Use `registerHandler` or `useBridgeCapability` for the same effect.                         |
| Bootstrap script injection | ✓                | ✓           | ✗                       | Cross-origin iframes can't have scripts injected from the host page. The iframe-side SDK loads itself as part of the journey bundle.                                                                                                                    |
| Origin allowlist           | Host-app         | Host-app    | SDK-level               | `postMessage` has no built-in URL gate. The SDK enforces `event.origin` on every inbound message and `targetOrigin` on every outbound send. iOS / Android leave URL policy to `WKNavigationDelegate` / `WebViewClient` at the navigation layer instead. |
| Send-while-detached        | Sets `lastError` | Silent drop | Silent drop             | Matches Android. Web has no equivalent to the iOS "WebView not attached" trigger — hook `delegate.onMessageSent` for observability.                                                                                                                     |
| Handler signature          | `async` only     | Sync only   | `void \| Promise<void>` | Both shapes work on web. Async rejections route to `delegate.onError`; sync throws are caught at the dispatch site and produce a `HANDLER_FAILURE` response so the journey isn't left waiting.                                                          |
| Delegate reference         | Weak             | Weak        | Strong                  | JavaScript has no integrator-grade weak-reference primitive. Direct integrators must set `host.delegate = undefined` before discarding the delegate's owner. `BridgeHostProvider` handles this automatically.                                           |

The full parity inventory (PARITY.md) lives in the internal SDK source repo.

## Design Rationale

The design choices below aren't obvious from the API surface alone. Each Q\&A explains why a specific decision was taken, and what it costs or buys the integrator.

### Why a message protocol instead of direct method calls?

A message protocol decouples the journey from the host. The journey doesn't need to know whether it's running inside an iOS WebView, an Android WebView, or an iframe in a web page — it sends the same messages regardless. This also allows the protocol to evolve without breaking existing integrations.

### Why declare capabilities upfront?

Capability declaration serves two purposes: it lets the journey adapt before hitting a dead end, and it gives the host explicit control over what features are exposed. A host page might run on a device with a camera but choose not to expose camera capture for a particular journey — for example, because the host's UX provides a richer capture flow than what the iframe can offer.

### Why separate declare and register on web (vs. typed slots on iOS / Android)?

iOS and Android can expose typed properties on a strongly-typed `BridgeHost` because their languages support it ergonomically (Swift's `@StateObject`, Kotlin's properties with backing fields). In a React + TypeScript host, the equivalent would require either generating a typed Provider for each capability or using non-idiomatic prop shapes. The `capabilities` prop + `useBridgeCapability` split keeps the React API conventional — declarative props for state, hooks for side effects — at the cost of one extra integration step. `registerCustomCapability(id, version, handler)` is available on the underlying host for callers who prefer the typed-slot ergonomics.

### Why an origin allowlist?

Unlike a native WebView (where the host has full control over what URL loads), an iframe can be repointed by any script in the host page. The `allowedOrigins` allowlist makes this safe: an attacker who repoints `iframe.src` to a hostile origin cannot send messages to or receive messages from the host's bridge. Origin validation is enforced on both inbound and outbound traffic for this reason.

### Why no bootstrap script injection?

iOS and Android inject a script into the native WebView at document start to expose `window.GBGBridge`. On web, the iframe is cross-origin — the host page cannot execute JavaScript inside it. The journey iframe loads its own SDK as part of its bundle. This is a hard browser constraint, not a design choice — but it also keeps the host-side surface simpler: there is no bootstrap configuration to manage.

### Why does the React Provider snapshot config?

`BridgeHost` instances are not free to rebuild — the message listener wiring, capability registration, and pending-request state would all reset. Snapshotting `allowedOrigins`, `hostVersion`, and `capabilities` at attach time means a parent re-render that changes those props (often unintentionally — e.g. a new array literal) does not silently rebuild the host. Integrators who genuinely need to apply runtime config changes use either `key`-based remount or the `registerCustomCapability` API.

### Why zero runtime dependencies?

GBGBridge sits on a hot path: every iframe message routes through it. Adding dependencies would increase bundle size, create version conflicts with consumer apps, and introduce supply-chain risk. The core uses only standard browser APIs (`postMessage`, `MessageEvent`, `URL`, `crypto.randomUUID` with fallback). The React package depends only on React itself, as a peer dependency.

## Next Steps

* [API Reference](/docs/go-v2/developer-integration/sdks/web/api-reference) — Detailed documentation for every public symbol.
* [Embedding](/docs/go-v2/developer-integration/sdks/web/embedding) — React, framework-agnostic, and SSR-aware integration patterns.
* [Messaging](/docs/go-v2/developer-integration/sdks/web/messaging) — Sending events and handling requests in depth.
* [Capability Handling](/docs/go-v2/developer-integration/sdks/web/capability-handling) — Declaring, registering, and dynamic capability negotiation.
