Integrations

Solving CAPTCHAs in React Native WebViews with CaptchaAI

React Native apps that load web content through react-native-webview frequently encounter CAPTCHAs — reCAPTCHA v2 checkboxes, Cloudflare Turnstile widgets, or challenge pages that block form submissions. CaptchaAI solves these challenges programmatically so your mobile app flows remain uninterrupted.

This guide shows you how to detect CAPTCHAs inside a React Native WebView, extract the required parameters via JavaScript injection, send them to the CaptchaAI API, and inject the solved token back into the page.

Real-World Scenario

You are building a React Native app that loads a third-party web form inside a WebView. The form includes a reCAPTCHA v2 checkbox that must be completed before submission. Your goal is to:

  1. Detect the CAPTCHA widget when the WebView finishes loading
  2. Extract the sitekey from the DOM
  3. Solve it via CaptchaAI's API from a backend service
  4. Inject the token back into the WebView and submit the form

Environment: React Native 0.72+, react-native-webview 13+, Node.js backend, CaptchaAI API.

Architecture Overview

The flow splits across three layers:

Layer Responsibility
React Native WebView Detects CAPTCHA, extracts sitekey, injects solved token
Backend API (Node.js) Receives sitekey + pageurl, calls CaptchaAI, returns token
CaptchaAI API Solves the CAPTCHA and returns the token

The WebView communicates with your React Native code via window.ReactNativeWebView.postMessage(), and your backend handles the CaptchaAI interaction to keep API keys off the client.

Step 1: Detect CAPTCHA and Extract Sitekey in WebView

Use the injectedJavaScript prop to scan for CAPTCHA elements once the page loads:

// CaptchaDetector.js — React Native Component
import React, { useRef, useState } from 'react';
import { View, ActivityIndicator } from 'react-native';
import { WebView } from 'react-native-webview';

const CAPTCHA_DETECTION_SCRIPT = `
  (function() {
    // Detect reCAPTCHA v2
    const recaptchaDiv = document.querySelector('.g-recaptcha');
    if (recaptchaDiv) {
      const sitekey = recaptchaDiv.getAttribute('data-sitekey');
      window.ReactNativeWebView.postMessage(JSON.stringify({
        type: 'captcha_detected',
        captchaType: 'recaptcha_v2',
        sitekey: sitekey,
        pageurl: window.location.href
      }));
      return;
    }

    // Detect Cloudflare Turnstile
    const turnstileDiv = document.querySelector('.cf-turnstile');
    if (turnstileDiv) {
      const sitekey = turnstileDiv.getAttribute('data-sitekey');
      window.ReactNativeWebView.postMessage(JSON.stringify({
        type: 'captcha_detected',
        captchaType: 'turnstile',
        sitekey: sitekey,
        pageurl: window.location.href
      }));
      return;
    }

    window.ReactNativeWebView.postMessage(JSON.stringify({
      type: 'no_captcha'
    }));
  })();
  true;
`;

export default function CaptchaWebView({ url }) {
  const webviewRef = useRef(null);
  const [solving, setSolving] = useState(false);

  const handleMessage = async (event) => {
    const data = JSON.parse(event.nativeEvent.data);

    if (data.type === 'captcha_detected') {
      setSolving(true);
      try {
        const token = await solveCaptchaViaBackend(
          data.captchaType,
          data.sitekey,
          data.pageurl
        );
        injectToken(data.captchaType, token);
      } catch (err) {
        console.error('CAPTCHA solve failed:', err.message);
      } finally {
        setSolving(false);
      }
    }
  };

  const solveCaptchaViaBackend = async (captchaType, sitekey, pageurl) => {
    const response = await fetch('https://your-backend.com/api/solve-captcha', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ captchaType, sitekey, pageurl }),
    });
    const result = await response.json();
    if (!result.token) throw new Error(result.error || 'No token returned');
    return result.token;
  };

  const injectToken = (captchaType, token) => {
    let script;
    if (captchaType === 'recaptcha_v2') {
      script = `
        document.getElementById('g-recaptcha-response').value = '${token}';
        if (typeof ___grecaptcha_cfg !== 'undefined') {
          Object.keys(___grecaptcha_cfg.clients).forEach(key => {
            const client = ___grecaptcha_cfg.clients[key];
            Object.keys(client).forEach(k => {
              const item = client[k];
              if (item && item.callback) {
                item.callback('${token}');
              }
            });
          });
        }
        true;
      `;
    } else if (captchaType === 'turnstile') {
      script = `
        const input = document.querySelector('[name="cf-turnstile-response"]');
        if (input) input.value = '${token}';
        const callback = document.querySelector('.cf-turnstile')
          ?.getAttribute('data-callback');
        if (callback && typeof window[callback] === 'function') {
          window[callback]('${token}');
        }
        true;
      `;
    }
    webviewRef.current?.injectJavaScript(script);
  };

  return (
    <View style={{ flex: 1 }}>
      {solving && <ActivityIndicator size="large" />}
      <WebView
        ref={webviewRef}
        source={{ uri: url }}
        injectedJavaScript={CAPTCHA_DETECTION_SCRIPT}
        onMessage={handleMessage}
        javaScriptEnabled={true}
      />
    </View>
  );
}

Step 2: Build the Backend Solver (Node.js)

Keep your CaptchaAI API key on the server side. The backend receives the sitekey and page URL, submits to CaptchaAI, polls for the result, and returns the token:

// server.js — Express backend
const express = require('express');
const axios = require('axios');
const app = express();
app.use(express.json());

const API_KEY = process.env.CAPTCHAAI_API_KEY || 'YOUR_API_KEY';

app.post('/api/solve-captcha', async (req, res) => {
  const { captchaType, sitekey, pageurl } = req.body;

  try {
    // Step 1: Submit task to CaptchaAI
    const submitParams = {
      key: API_KEY,
      pageurl: pageurl,
      json: '1',
    };

    if (captchaType === 'recaptcha_v2') {
      submitParams.method = 'userrecaptcha';
      submitParams.googlekey = sitekey;
    } else if (captchaType === 'turnstile') {
      submitParams.method = 'turnstile';
      submitParams.sitekey = sitekey;
    }

    const submitResponse = await axios.get(
      'https://ocr.captchaai.com/in.php',
      { params: submitParams }
    );

    if (submitResponse.data.status !== 1) {
      return res.status(400).json({ error: submitResponse.data.request });
    }

    const taskId = submitResponse.data.request;

    // Step 2: Poll for result
    const token = await pollForResult(taskId);
    res.json({ token });
  } catch (error) {
    console.error('Solve error:', error.message);
    res.status(500).json({ error: 'Failed to solve CAPTCHA' });
  }
});

async function pollForResult(taskId, maxAttempts = 30) {
  for (let i = 0; i < maxAttempts; i++) {
    await new Promise((r) => setTimeout(r, 5000));

    const response = await axios.get('https://ocr.captchaai.com/res.php', {
      params: {
        key: API_KEY,
        action: 'get',
        id: taskId,
        json: '1',
      },
    });

    if (response.data.status === 1) {
      return response.data.request;
    }

    if (
      response.data.request !== 'CAPCHA_NOT_READY' &&
      response.data.status === 0
    ) {
      throw new Error(response.data.request);
    }
  }
  throw new Error('Polling timeout — CAPTCHA not solved in time');
}

app.listen(3000, () => console.log('Solver backend running on port 3000'));

Step 3: Handle Token Expiration in WebView

CAPTCHA tokens expire — reCAPTCHA v2 tokens last ~120 seconds, Turnstile tokens ~300 seconds. If the user delays form submission, re-solve before submitting:

// Add to CaptchaWebView component
const [tokenTimestamp, setTokenTimestamp] = useState(null);
const TOKEN_TTL_MS = 110000; // 110 seconds for reCAPTCHA v2

const handleFormSubmit = async (captchaType, sitekey, pageurl) => {
  const now = Date.now();
  if (!tokenTimestamp || now - tokenTimestamp > TOKEN_TTL_MS) {
    const freshToken = await solveCaptchaViaBackend(
      captchaType, sitekey, pageurl
    );
    injectToken(captchaType, freshToken);
    setTokenTimestamp(Date.now());
  }

  webviewRef.current?.injectJavaScript(`
    document.querySelector('form').submit();
    true;
  `);
};

Troubleshooting

Problem Cause Fix
postMessage not received WebView onMessage not set or script error Check onMessage is bound; wrap injection script in try/catch
ERROR_BAD_TOKEN_OR_PAGEURL Sitekey doesn't match the page URL Extract sitekey from the actual iframe src, not the parent page
Token injection doesn't trigger callback reCAPTCHA callback not found in ___grecaptcha_cfg Iterate all client objects and check nested properties for callback functions
CAPCHA_NOT_READY indefinitely Slow solve or invalid parameters Increase polling timeout; verify sitekey and pageurl are correct
WebView shows blank CAPTCHA JavaScript disabled or content blocked Set javaScriptEnabled={true} and ensure no content security policy blocks

FAQ

Can I call CaptchaAI directly from React Native without a backend?

Technically yes, but your API key would be exposed in the app binary. Always route API calls through your own backend to keep credentials secure.

Does this work with Expo managed workflow?

Yes, react-native-webview is supported in Expo SDK 49+ with the expo-dev-client. The JavaScript injection and message passing work identically.

How do I handle pages with both reCAPTCHA and Turnstile?

The detection script checks for both. If a page has multiple CAPTCHAs, extend the script to collect all sitekeys and solve them sequentially before form submission.

What is the average solve time for mobile CAPTCHAs?

reCAPTCHA v2 solves typically take 10-20 seconds. Cloudflare Turnstile is faster at 5-15 seconds. Plan your UX to show a loading indicator during this window.

Does the User-Agent of the WebView affect solve rates?

React Native WebView uses the device's default mobile User-Agent, which aligns well with how real users browse. This generally produces good solve rates without modification.

Next Steps

Start solving CAPTCHAs in your React Native apps — get your CaptchaAI API key and integrate the backend solver today.

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.