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:

Discussions (0)

No comments yet.

Related Posts

Comparisons WebDriver vs Chrome DevTools Protocol for CAPTCHA Automation
Compare Web Driver and Chrome Dev Tools Protocol (CDP) for CAPTCHA automation — detection, performance, capabilities, and when to use each with Captcha AI.

Compare Web Driver and Chrome Dev Tools Protocol (CDP) for CAPTCHA automation — detection, performance, capabi...

Automation Python reCAPTCHA v2
Mar 27, 2026
Reference CAPTCHA Token Injection Methods Reference
Complete reference for injecting solved CAPTCHA tokens into web pages.

Complete reference for injecting solved CAPTCHA tokens into web pages. Covers re CAPTCHA, Turnstile, and Cloud...

Automation Python reCAPTCHA v2
Apr 08, 2026
Tutorials Pytest Fixtures for CaptchaAI API Testing
Build reusable pytest fixtures to test CAPTCHA-solving workflows with Captcha AI.

Build reusable pytest fixtures to test CAPTCHA-solving workflows with Captcha AI. Covers mocking, live integra...

Automation Python reCAPTCHA v2
Apr 08, 2026
Tutorials CAPTCHA Solving Fallback Chains
Implement fallback chains for CAPTCHA solving with Captcha AI.

Implement fallback chains for CAPTCHA solving with Captcha AI. Cascade through solver methods, proxy pools, an...

Automation Python reCAPTCHA v2
Apr 06, 2026
Use Cases Multi-Step Workflow Automation with CaptchaAI
Manage workflows across multiple accounts on CAPTCHA-protected platforms — , action, and data collection at scale.

Manage workflows across multiple accounts on CAPTCHA-protected platforms — , action, and data collection at sc...

Automation Python reCAPTCHA v2
Apr 06, 2026
Troubleshooting ERROR_PAGEURL: URL Mismatch Troubleshooting Guide
Fix ERROR_PAGEURL when using Captcha AI.

Fix ERROR_PAGEURL when using Captcha AI. Diagnose URL mismatch issues, handle redirects, SPAs, and dynamic URL...

Automation Python reCAPTCHA v2
Mar 23, 2026
Troubleshooting Handling reCAPTCHA v2 and Cloudflare Turnstile on the Same Site
Solve both re CAPTCHA v 2 and Cloudflare Turnstile on sites that use multiple CAPTCHA providers — detect which type appears, solve each correctly, and handle pr...

Solve both re CAPTCHA v 2 and Cloudflare Turnstile on sites that use multiple CAPTCHA providers — detect which...

Automation Python reCAPTCHA v2
Mar 23, 2026
Integrations Solving CAPTCHAs in React Native WebViews with CaptchaAI
how to detect and solve re CAPTCHA v 2 and Cloudflare Turnstile CAPTCHAs inside React Native Web Views using the Captcha AI API with working Java Script bridge...

Learn how to detect and solve re CAPTCHA v 2 and Cloudflare Turnstile CAPTCHAs inside React Native Web Views u...

Automation Python reCAPTCHA v2
Mar 30, 2026
API Tutorials CaptchaAI API Latency Optimization: Faster Solves
Reduce CAPTCHA solve latency with Captcha AI by optimizing poll intervals, connection pooling, prefetching, and proxy selection.

Reduce CAPTCHA solve latency with Captcha AI by optimizing poll intervals, connection pooling, prefetching, an...

Automation Python reCAPTCHA v2
Feb 27, 2026
Tutorials Building Client CAPTCHA Pipelines with CaptchaAI
Build production-ready CAPTCHA-solving pipelines for client projects using Captcha AI.

Build production-ready CAPTCHA-solving pipelines for client projects using Captcha AI. Covers architecture, qu...

Automation Python reCAPTCHA v2
Feb 18, 2026
Integrations Axios + CaptchaAI: Solve CAPTCHAs Without a Browser
Use Axios and Captcha AI to solve re CAPTCHA, Turnstile, and image CAPTCHAs in Node.js without launching a browser.

Use Axios and Captcha AI to solve re CAPTCHA, Turnstile, and image CAPTCHAs in Node.js without launching a bro...

Automation All CAPTCHA Types
Apr 08, 2026
Integrations Scrapy + CaptchaAI Integration Guide
Integrate Captcha AI into Scrapy spiders to automatically solve CAPTCHAs during web crawling with middleware and signal handlers.

Integrate Captcha AI into Scrapy spiders to automatically solve CAPTCHAs during web crawling with middleware a...

Automation reCAPTCHA v2 Scrapy
Jan 27, 2026
Integrations Puppeteer Stealth + CaptchaAI: Reliable Browser Automation
Standard Puppeteer gets detected immediately by anti-bot systems.

Standard Puppeteer gets detected immediately by anti-bot systems. `puppeteer-extra-plugin-stealth` patches the...

Automation reCAPTCHA v2 Cloudflare Turnstile
Apr 05, 2026