Flutter apps loading web content through webview_flutter or flutter_inappwebview regularly encounter CAPTCHAs that block user flows. CaptchaAI solves these challenges via API, letting your Flutter app detect, solve, and inject CAPTCHA tokens automatically inside WebViews.
This guide covers CAPTCHA detection via JavaScript channels, backend solver integration, and token injection for reCAPTCHA v2 and Cloudflare Turnstile.
Real-World Scenario
Your Flutter app embeds a payment gateway in a WebView. The gateway presents a reCAPTCHA v2 challenge before processing. You need to:
- Detect the CAPTCHA widget after the WebView loads
- Extract the sitekey through a JavaScript channel
- Solve it via CaptchaAI from a backend service
- Inject the token and trigger the callback
Environment: Flutter 3.16+, webview_flutter 4.x, Dart backend or Node.js API, CaptchaAI API.
Step 1: Set Up WebView with JavaScript Channels
Use webview_flutter with a JavaScript channel to receive CAPTCHA detection messages from the loaded page:
// captcha_webview.dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:http/http.dart' as http;
class CaptchaWebView extends StatefulWidget {
final String url;
const CaptchaWebView({super.key, required this.url});
@override
State<CaptchaWebView> createState() => _CaptchaWebViewState();
}
class _CaptchaWebViewState extends State<CaptchaWebView> {
late final WebViewController _controller;
bool _solving = false;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel(
'CaptchaChannel',
onMessageReceived: _onCaptchaMessage,
)
..setNavigationDelegate(
NavigationDelegate(
onPageFinished: (_) => _detectCaptcha(),
),
)
..loadRequest(Uri.parse(widget.url));
}
Future<void> _detectCaptcha() async {
await _controller.runJavaScript('''
(function() {
var recaptcha = document.querySelector('.g-recaptcha');
if (recaptcha) {
CaptchaChannel.postMessage(JSON.stringify({
type: 'captcha_detected',
captchaType: 'recaptcha_v2',
sitekey: recaptcha.getAttribute('data-sitekey'),
pageurl: window.location.href
}));
return;
}
var turnstile = document.querySelector('.cf-turnstile');
if (turnstile) {
CaptchaChannel.postMessage(JSON.stringify({
type: 'captcha_detected',
captchaType: 'turnstile',
sitekey: turnstile.getAttribute('data-sitekey'),
pageurl: window.location.href
}));
return;
}
CaptchaChannel.postMessage(JSON.stringify({type: 'no_captcha'}));
})();
''');
}
Future<void> _onCaptchaMessage(JavaScriptMessage message) async {
final data = jsonDecode(message.message);
if (data['type'] != 'captcha_detected') return;
setState(() => _solving = true);
try {
final token = await _solveCaptcha(
data['captchaType'],
data['sitekey'],
data['pageurl'],
);
await _injectToken(data['captchaType'], token);
} catch (e) {
debugPrint('CAPTCHA solve failed: $e');
} finally {
setState(() => _solving = false);
}
}
Future<String> _solveCaptcha(
String captchaType, String sitekey, String pageurl,
) async {
final response = await http.post(
Uri.parse('https://your-backend.com/api/solve-captcha'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'captchaType': captchaType,
'sitekey': sitekey,
'pageurl': pageurl,
}),
);
final result = jsonDecode(response.body);
if (result['token'] == null) {
throw Exception(result['error'] ?? 'No token returned');
}
return result['token'];
}
Future<void> _injectToken(String captchaType, String token) async {
if (captchaType == 'recaptcha_v2') {
await _controller.runJavaScript('''
document.getElementById('g-recaptcha-response').value = '$token';
if (typeof ___grecaptcha_cfg !== 'undefined') {
Object.keys(___grecaptcha_cfg.clients).forEach(function(key) {
var client = ___grecaptcha_cfg.clients[key];
Object.keys(client).forEach(function(k) {
if (client[k] && client[k].callback) {
client[k].callback('$token');
}
});
});
}
''');
} else if (captchaType == 'turnstile') {
await _controller.runJavaScript('''
var input = document.querySelector('[name="cf-turnstile-response"]');
if (input) input.value = '$token';
var cb = document.querySelector('.cf-turnstile')
?.getAttribute('data-callback');
if (cb && typeof window[cb] === 'function') window[cb]('$token');
''');
}
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
WebViewWidget(controller: _controller),
if (_solving)
const Center(child: CircularProgressIndicator()),
],
);
}
}
Step 2: Backend Solver (Python)
The backend keeps your API key secure and handles CaptchaAI communication:
# solver_api.py — Flask backend
import os
import time
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
API_KEY = os.environ.get("CAPTCHAAI_API_KEY", "YOUR_API_KEY")
@app.route("/api/solve-captcha", methods=["POST"])
def solve_captcha():
data = request.json
captcha_type = data.get("captchaType")
sitekey = data.get("sitekey")
pageurl = data.get("pageurl")
# Submit task
params = {"key": API_KEY, "pageurl": pageurl, "json": "1"}
if captcha_type == "recaptcha_v2":
params["method"] = "userrecaptcha"
params["googlekey"] = sitekey
elif captcha_type == "turnstile":
params["method"] = "turnstile"
params["sitekey"] = sitekey
else:
return jsonify({"error": f"Unsupported type: {captcha_type}"}), 400
resp = requests.get("https://ocr.captchaai.com/in.php", params=params)
result = resp.json()
if result.get("status") != 1:
return jsonify({"error": result.get("request", "Submit failed")}), 400
task_id = result["request"]
# Poll for result
for _ in range(30):
time.sleep(5)
poll_resp = requests.get(
"https://ocr.captchaai.com/res.php",
params={
"key": API_KEY,
"action": "get",
"id": task_id,
"json": "1",
},
)
poll_result = poll_resp.json()
if poll_result.get("status") == 1:
return jsonify({"token": poll_result["request"]})
if poll_result.get("request") != "CAPCHA_NOT_READY":
return jsonify({"error": poll_result["request"]}), 400
return jsonify({"error": "Timeout — CAPTCHA not solved"}), 408
if __name__ == "__main__":
app.run(port=3000)
Step 3: Using flutter_inappwebview (Alternative)
If you need more control — intercept network requests, handle cookies, or manage multiple WebViews — use flutter_inappwebview:
// Using flutter_inappwebview for advanced CAPTCHA handling
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
InAppWebView(
initialUrlRequest: URLRequest(url: WebUri(widget.url)),
initialSettings: InAppWebViewSettings(
javaScriptEnabled: true,
userAgent: 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36',
),
onLoadStop: (controller, url) async {
// Evaluate JavaScript and get result directly
final result = await controller.evaluateJavascript(source: '''
(function() {
var el = document.querySelector('.g-recaptcha');
if (el) return JSON.stringify({
sitekey: el.getAttribute('data-sitekey'),
pageurl: window.location.href
});
return null;
})();
''');
if (result != null) {
final data = jsonDecode(result);
// Solve and inject token
final token = await _solveCaptcha(
'recaptcha_v2', data['sitekey'], data['pageurl'],
);
await controller.evaluateJavascript(source: '''
document.getElementById('g-recaptcha-response').value = '$token';
''');
}
},
)
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| JavaScript channel not receiving messages | Channel name mismatch | Ensure CaptchaChannel matches exactly between Dart and JS |
ERROR_BAD_TOKEN_OR_PAGEURL from CaptchaAI |
Sitekey from wrong iframe | Extract sitekey from the CAPTCHA iframe, not the parent frame |
| Token injection has no effect | Textarea hidden or callback not triggered | Set g-recaptcha-response value AND fire the callback function |
CAPCHA_NOT_READY keeps polling |
Slow solve or invalid parameters | Verify sitekey and pageurl; increase maximum polling attempts |
| WebView crashes on CAPTCHA page | Memory issues with heavy pages | Use flutter_inappwebview with useHybridComposition: true on Android |
FAQ
Should I use webview_flutter or flutter_inappwebview?
webview_flutter covers most cases. Use flutter_inappwebview when you need cookie management, request interception, or direct JavaScript evaluation with return values.
Can I solve CAPTCHAs without a backend server?
You could call CaptchaAI directly from Dart, but this exposes your API key in the app binary. Always route through a backend for production apps.
How do I handle CAPTCHA token expiration in Flutter?
reCAPTCHA v2 tokens expire in ~120 seconds. Track when the token was obtained and re-solve if the user hasn't submitted the form within that window.
Does this work on both Android and iOS?
Yes. Both webview_flutter and flutter_inappwebview support Android and iOS. JavaScript injection and channel communication work identically on both platforms.
Related Articles
- How To Solve Recaptcha V2 Callback Using Api
- Geetest Vs Cloudflare Turnstile Comparison
- Recaptcha V2 Turnstile Same Site Handling
Next Steps
Start solving CAPTCHAs in your Flutter apps — get your CaptchaAI API key and connect your backend solver.
Related guides:
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.