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-memoryOmit the game parameters for the plain checkbox. A games=a,b list picks one at random per session.
Query parameters
| Parameter | Required | Effect |
|---|---|---|
sitekey | yes | Your public key (cpt_pub_...). Missing renders an error shell with no widget. |
game | no | A single marketplace game id (owner/repo, or owner/repo/leaf). |
games | no | A comma-separated list; one is chosen at random per session. |
game-src | no | URL of your own hosted game bundle, for a self-hosted game. |
layout | no | inline, modal, fullscreen, or auto. Defaults to inline on the embed. |
locale / skin / config | no | Forwarded to the widget to pick a language, skin, or configuration. |
no-verify | no | Runs the game with no verification gate (game only, no token). |
postMessageTarget | no | Browser-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
- Server side integration for the token check.
- Client side integration for the web equivalent.