---
type: how-to
---

# Embed Caputchin in a mobile app via WebView

Reference for the pattern is [mobile](../mobile.md). The widget is the same as the web version; only the host shell differs.

## 1. Open the embed page in a WebView

The platform hosts a mobile-optimized embed page that wraps the [widget](../widget.md) with mobile-friendly defaults (full viewport, touch-tuned).

URL shape:

```
https://embed.caputchin.com/?sitekey=cpt_pub_...&game=bubble-pop
```

Or with a pool:

```
https://embed.caputchin.com/?sitekey=cpt_pub_...&games=bubble-pop,memory-match
```

## 2. Bridge the wrapped token back to native code

The embed page emits the wrapped token via `window.postMessage`. Your WebView host listens for it.

### iOS (WKWebView, Swift)

```swift
class TokenHandler: NSObject, WKScriptMessageHandler {
  func userContentController(_ controller: WKUserContentController,
                             didReceive message: WKScriptMessage) {
    guard let token = message.body as? String else { return }
    // send `token` to your backend
  }
}

let config = WKWebViewConfiguration()
config.userContentController.add(TokenHandler(), name: "caputchin")
let webView = WKWebView(frame: .zero, configuration: config)
webView.load(URLRequest(url: URL(string: "https://embed.caputchin.com/?sitekey=...&game=...")!))
```

### Android (WebView, Kotlin)

```kotlin
class TokenBridge {
  @JavascriptInterface
  fun onToken(token: String) {
    // send `token` to your backend
  }
}

webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(TokenBridge(), "caputchin")
webView.loadUrl("https://embed.caputchin.com/?sitekey=...&game=...")
```

## 3. Verify on your backend

Same as web — POST the token to `/siteverify` with your secret. See the [verify-server-side guide](verify-server-side.md).

## Receiving widget errors (optional)

The embed page also calls a second native handler — `caputchinError` — whenever the widget fires its `error` event. The handler receives a JSON-stringified payload with the same fields as the [widget `error` event detail](../widget.md#events): `{ code, message, originalCode? }`. The native side may parse it to log, retry, or branch.

Wiring is symmetric to the token handler and entirely optional — apps that don't register `caputchinError` simply don't receive errors; the embed page already shows a retry UI inside the WebView regardless.

### iOS (WKWebView, Swift)

```swift
class ErrorHandler: NSObject, WKScriptMessageHandler {
  func userContentController(_ controller: WKUserContentController,
                             didReceive message: WKScriptMessage) {
    guard let json = message.body as? String,
          let data = json.data(using: .utf8),
          let payload = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return }
    // payload["code"], payload["message"], payload["originalCode"] (optional)
  }
}

config.userContentController.add(ErrorHandler(), name: "caputchinError")
```

### Android (WebView, Kotlin)

```kotlin
class ErrorBridge {
  @JavascriptInterface
  fun onError(payloadJson: String) {
    val payload = JSONObject(payloadJson)
    // payload.getString("code"), payload.getString("message"),
    // payload.optString("originalCode", null)
  }
}

webView.addJavascriptInterface(ErrorBridge(), "caputchinError")
```

## What you don't need to do

- **No native PoW.** Cap stays in the WebView. See [ADR-0005](../adr/0005-webview-only-mobile.md).
- **No mobile-specific game code.** Game authors write one `register()` call that runs on both platforms.
- **No paid mobile tier.** Mobile is free across all tiers — see [mobile](../mobile.md).

## When native SDKs ship later

A future thin native wrapper (Swift / Kotlin) will encapsulate this WebView setup. It will be free, since it's a DX upgrade over the same WebView. Build slot earned when customer demand emerges — see [roadmap](../roadmap.md#whats-deferred-entirely).
