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:
- Detect the CAPTCHA widget when the WebView finishes loading
- Extract the sitekey from the DOM
- Solve it via CaptchaAI's API from a backend service
- 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.
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 React Native apps — get your CaptchaAI API key and integrate the backend solver today.
Related guides:
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.