Caputchin
Integration guides

Mobile apps integration

Caputchin runs inside a native app through a hosted embed page. You point a WebView at that page, the visitor clears the challenge inside it, and the page bridges the resulting token (and any error) back to your native code. The verification itself is identical to the web; only the host shell differs.

1. Open the embed page

Point a WebView at https://caputchin.com/embed with at least a site key:

https://caputchin.com/embed?sitekey=cpt_pub_...&game=caputchin/games/leaf-memory

Omit the game parameters for the plain checkbox. A games=a,b list picks one at random per session.

Query parameters

ParameterRequiredEffect
sitekeyyesYour public key (cpt_pub_...). Missing renders an error shell with no widget.
gamenoA single marketplace game id (owner/repo, or owner/repo/leaf).
gamesnoA comma-separated list; one is chosen at random per session.
game-srcnoURL of your own hosted game bundle, for a self-hosted game.
layoutnoinline, modal, fullscreen, or auto. Defaults to inline on the embed.
locale / skin / confignoForwarded to the widget to pick a language, skin, or configuration.
no-verifynoRuns the game with no verification gate (game only, no token).
postMessageTargetnoBrowser-iframe embedders only (see below). Ignored in a native WebView.

Omit all three of game / games / game-src for the plain checkbox. There is no mode parameter; the widget drives the trigger itself.

2. Bridge the token to your code

When the visitor passes, the embed page pushes the token through the channel that matches your host. Register the matching handler.

iOS (WKWebView, Swift)

The token arrives as a raw string on a message handler named caputchin:

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://caputchin.com/embed?sitekey=cpt_pub_...&game=caputchin/games/leaf-memory")!))

Android (WebView, Kotlin)

The token arrives as a raw string on a @JavascriptInterface named caputchin:

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

webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(TokenBridge(), "caputchin")
webView.loadUrl("https://caputchin.com/embed?sitekey=cpt_pub_...&game=caputchin/games/leaf-memory")

React Native (react-native-webview)

The embed posts a JSON string on the single React Native channel. Parse it and branch on type:

<WebView
  source={{ uri: "https://caputchin.com/embed?sitekey=cpt_pub_...&game=caputchin/games/leaf-memory" }}
  onMessage={(e) => {
    const msg = JSON.parse(e.nativeEvent.data);
    if (msg.type === "caputchin-token") {
      // send msg.token to your backend
    } else if (msg.type === "caputchin-error") {
      // msg.code, msg.message, msg.originalCode (optional)
    }
  }}
/>

Browser iframe (postMessage)

If you embed the page in a browser <iframe> rather than a native WebView, the parent relay is off by default and fires only when you opt in with an exact target origin via ?postMessageTarget=<origin> (for example postMessageTarget=https://app.example.com). A wildcard (*), a path, or anything that does not parse to a bare scheme, host, and port is rejected, so the single-use, site-bound token is never broadcast to an unverified parent. Listen on the parent and check event.origin before trusting the message:

window.addEventListener("message", (event) => {
  if (event.origin !== "https://embed-host.example.com") return;
  const msg = event.data;
  if (msg.type === "caputchin-token") {
    // msg.token
  } else if (msg.type === "caputchin-error") {
    // msg.code, msg.message
  }
});

3. Verify on your backend

This is the same as the web: POST the token to /siteverify with your secret. See Server side integration.

Receiving errors (optional)

Whenever the widget hits an error, the embed page mirrors it through a parallel set of channels: iOS caputchinError message handler, Android caputchinError interface (onError(json)), the React Native channel as { type: "caputchin-error", ... }, and the same gated iframe relay. The payload carries { code, message, originalCode? }. iOS and Android receive it as a JSON string; React Native and the iframe relay receive the typed envelope.

This is entirely optional. An app that registers no error handler simply does not receive errors, and the embed page already shows a retry UI inside the WebView regardless.

See also

On this page