Integrations

CAPTCHA Handling in Flutter WebViews with CaptchaAI

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:

  1. Detect the CAPTCHA widget after the WebView loads
  2. Extract the sitekey through a JavaScript channel
  3. Solve it via CaptchaAI from a backend service
  4. 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.

Next Steps

Start solving CAPTCHAs in your Flutter apps — get your CaptchaAI API key and connect your backend solver.

Related guides:

Full Working Code

Complete runnable examples for this article in Python, Node.js, PHP, Go, Java, C#, Ruby, Rust, Kotlin & Bash.

View on GitHub →

Discussions (0)

No comments yet.