Dart developers building Flutter apps, backend services with Dart Frog or Shelf, or CLI automation tools encounter CAPTCHAs in form submissions and API interactions. CaptchaAI's HTTP API integrates natively with Dart's async/await model through the http package or dio.
This guide covers reCAPTCHA v2/v3, Cloudflare Turnstile, and image CAPTCHA solving — with both pure Dart and Flutter-specific implementations.
Why Dart for CAPTCHA Integration
- Async-first —
Future,async/await, andStreamhandle API polling naturally - Single codebase — Flutter shares code between iOS, Android, web, and desktop
- Strong typing — catch API response issues at compile time
- Server-side Dart — use the same solver on backend with dart_frog or shelf
- Isolates — true parallel solving without shared-state complexity
Prerequisites
Add to pubspec.yaml:
dependencies:
http: ^1.2.0
dio: ^5.4.0 # optional, alternative HTTP client
image: ^4.2.0 # optional, for image processing
Run:
dart pub get
# or for Flutter:
flutter pub get
Data Models
enum CaptchaType { recaptchaV2, recaptchaV3, turnstile, imageBase64 }
class CaptchaTask {
final CaptchaType type;
final String? sitekey;
final String? pageUrl;
final String? action;
final double? minScore;
final String? imageBody;
CaptchaTask.recaptchaV2({required this.sitekey, required this.pageUrl})
: type = CaptchaType.recaptchaV2,
action = null,
minScore = null,
imageBody = null;
CaptchaTask.recaptchaV3({
required this.sitekey,
required this.pageUrl,
this.action = 'verify',
this.minScore = 0.7,
}) : type = CaptchaType.recaptchaV3,
imageBody = null;
CaptchaTask.turnstile({required this.sitekey, required this.pageUrl})
: type = CaptchaType.turnstile,
action = null,
minScore = null,
imageBody = null;
CaptchaTask.imageBase64({required this.imageBody})
: type = CaptchaType.imageBase64,
sitekey = null,
pageUrl = null,
action = null,
minScore = null;
}
class CaptchaException implements Exception {
final String message;
CaptchaException(this.message);
@override
String toString() => 'CaptchaException: $message';
}
Core Solver — http Package
import 'dart:convert';
import 'dart:async';
import 'package:http/http.dart' as http;
class CaptchaSolver {
final String apiKey;
final String _baseUrl = 'https://ocr.captchaai.com';
final Duration pollInterval;
final Duration maxWait;
final http.Client _client;
CaptchaSolver(
this.apiKey, {
this.pollInterval = const Duration(seconds: 5),
this.maxWait = const Duration(seconds: 300),
http.Client? client,
}) : _client = client ?? http.Client();
/// Solve any CAPTCHA type
Future<String> solve(CaptchaTask task) async {
final taskId = await _submit(task);
return _poll(taskId);
}
/// Check account balance
Future<double> checkBalance() async {
final uri = Uri.parse('$_baseUrl/res.php').replace(queryParameters: {
'key': apiKey,
'action': 'getbalance',
'json': '1',
});
final response = await _client.get(uri);
final data = jsonDecode(response.body);
return double.parse(data['request'].toString());
}
Future<String> _submit(CaptchaTask task) async {
final params = <String, String>{
'key': apiKey,
'json': '1',
};
switch (task.type) {
case CaptchaType.recaptchaV2:
params['method'] = 'userrecaptcha';
params['googlekey'] = task.sitekey!;
params['pageurl'] = task.pageUrl!;
break;
case CaptchaType.recaptchaV3:
params['method'] = 'userrecaptcha';
params['googlekey'] = task.sitekey!;
params['pageurl'] = task.pageUrl!;
params['version'] = 'v3';
params['action'] = task.action!;
params['min_score'] = task.minScore.toString();
break;
case CaptchaType.turnstile:
params['method'] = 'turnstile';
params['key'] = task.sitekey!;
params['pageurl'] = task.pageUrl!;
break;
case CaptchaType.imageBase64:
params['method'] = 'base64';
params['body'] = task.imageBody!;
break;
}
final response = await _client.post(
Uri.parse('$_baseUrl/in.php'),
body: params,
);
final data = jsonDecode(response.body);
if (data['status'] != 1) {
throw CaptchaException('Submit failed: ${data['request']}');
}
return data['request'];
}
Future<String> _poll(String taskId) async {
final deadline = DateTime.now().add(maxWait);
while (DateTime.now().isBefore(deadline)) {
await Future.delayed(pollInterval);
final uri = Uri.parse('$_baseUrl/res.php').replace(queryParameters: {
'key': apiKey,
'action': 'get',
'id': taskId,
'json': '1',
});
final response = await _client.get(uri);
final data = jsonDecode(response.body);
if (data['request'] == 'CAPCHA_NOT_READY') continue;
if (data['status'] != 1) {
throw CaptchaException('Solve failed: ${data['request']}');
}
return data['request'];
}
throw CaptchaException('Timeout waiting for solution');
}
void dispose() => _client.close();
}
Usage
Future<void> main() async {
final solver = CaptchaSolver('YOUR_API_KEY');
try {
// Check balance
final balance = await solver.checkBalance();
print('Balance: \$${balance.toStringAsFixed(2)}');
// Solve reCAPTCHA v2
final token = await solver.solve(CaptchaTask.recaptchaV2(
sitekey: '6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-',
pageUrl: 'https://example.com/login',
));
print('Token: ${token.substring(0, 50)}...');
// Solve Turnstile
final turnstile = await solver.solve(CaptchaTask.turnstile(
sitekey: '0x4AAAAAAAB5...',
pageUrl: 'https://example.com/form',
));
print('Turnstile: ${turnstile.substring(0, 50)}...');
} finally {
solver.dispose();
}
}
Image CAPTCHA Solving
import 'dart:io';
import 'dart:convert';
Future<String> solveImageFile(CaptchaSolver solver, String path) async {
final bytes = await File(path).readAsBytes();
final base64Image = base64Encode(bytes);
return solver.solve(CaptchaTask.imageBase64(imageBody: base64Image));
}
// From network URL
Future<String> solveImageUrl(CaptchaSolver solver, String imageUrl) async {
final client = http.Client();
final response = await client.get(Uri.parse(imageUrl));
final base64Image = base64Encode(response.bodyBytes);
client.close();
return solver.solve(CaptchaTask.imageBase64(imageBody: base64Image));
}
Concurrent Solving
Future<List<String>> solveBatch(
CaptchaSolver solver,
List<CaptchaTask> tasks, {
int concurrency = 5,
}) async {
final results = <String>[];
// Simple parallel: all at once
final futures = tasks.map((task) => solver.solve(task));
final resolved = await Future.wait(
futures,
eagerError: false,
);
return resolved;
}
// With error handling
Future<List<Result<String>>> solveBatchSafe(
CaptchaSolver solver,
List<CaptchaTask> tasks,
) async {
final futures = tasks.map((task) async {
try {
final token = await solver.solve(task);
return Result.success(token);
} catch (e) {
return Result.failure(e.toString());
}
});
return Future.wait(futures.toList());
}
class Result<T> {
final T? value;
final String? error;
final bool isSuccess;
Result.success(this.value)
: error = null,
isSuccess = true;
Result.failure(this.error)
: value = null,
isSuccess = false;
}
Solving with Isolates (True Parallelism)
import 'dart:isolate';
Future<String> solveInIsolate(String apiKey, CaptchaTask task) async {
return Isolate.run(() async {
final solver = CaptchaSolver(apiKey);
try {
return await solver.solve(task);
} finally {
solver.dispose();
}
});
}
// Parallel solving across isolates
Future<List<String>> solveParallel(
String apiKey,
List<CaptchaTask> tasks,
) async {
final futures = tasks.map((task) => solveInIsolate(apiKey, task));
return Future.wait(futures.toList());
}
Flutter Integration
Provider/Service
import 'package:flutter/foundation.dart';
class CaptchaService extends ChangeNotifier {
final CaptchaSolver _solver;
String? _token;
String? _error;
bool _isLoading = false;
String? get token => _token;
String? get error => _error;
bool get isLoading => _isLoading;
CaptchaService(String apiKey) : _solver = CaptchaSolver(apiKey);
Future<void> solveRecaptcha(String sitekey, String pageUrl) async {
_isLoading = true;
_token = null;
_error = null;
notifyListeners();
try {
_token = await _solver.solve(CaptchaTask.recaptchaV2(
sitekey: sitekey,
pageUrl: pageUrl,
));
} catch (e) {
_error = e.toString();
}
_isLoading = false;
notifyListeners();
}
@override
void dispose() {
_solver.dispose();
super.dispose();
}
}
Widget
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class CaptchaButton extends StatelessWidget {
final String sitekey;
final String pageUrl;
const CaptchaButton({
required this.sitekey,
required this.pageUrl,
super.key,
});
@override
Widget build(BuildContext context) {
return Consumer<CaptchaService>(
builder: (context, service, child) {
if (service.isLoading) {
return const Column(
children: [
CircularProgressIndicator(),
SizedBox(height: 8),
Text('Solving CAPTCHA...'),
],
);
}
if (service.token != null) {
return const Icon(Icons.check_circle, color: Colors.green, size: 48);
}
return Column(
children: [
ElevatedButton(
onPressed: () => service.solveRecaptcha(sitekey, pageUrl),
child: const Text('Solve CAPTCHA'),
),
if (service.error != null)
Text(service.error!, style: const TextStyle(color: Colors.red)),
],
);
},
);
}
}
Dio Alternative
import 'package:dio/dio.dart';
class DioCaptchaSolver {
final String apiKey;
final Dio _dio;
DioCaptchaSolver(this.apiKey)
: _dio = Dio(BaseOptions(
baseUrl: 'https://ocr.captchaai.com',
connectTimeout: const Duration(seconds: 30),
receiveTimeout: const Duration(seconds: 30),
));
Future<String> solveRecaptchaV2(String siteUrl, String sitekey) async {
final submitResp = await _dio.post('/in.php', data: {
'key': apiKey,
'json': 1,
'method': 'userrecaptcha',
'googlekey': sitekey,
'pageurl': siteUrl,
}, options: Options(contentType: Headers.formUrlEncodedContentType));
if (submitResp.data['status'] != 1) {
throw CaptchaException('Submit: ${submitResp.data['request']}');
}
final taskId = submitResp.data['request'];
final deadline = DateTime.now().add(const Duration(seconds: 300));
while (DateTime.now().isBefore(deadline)) {
await Future.delayed(const Duration(seconds: 5));
final pollResp = await _dio.get('/res.php', queryParameters: {
'key': apiKey,
'action': 'get',
'id': taskId,
'json': 1,
});
if (pollResp.data['request'] == 'CAPCHA_NOT_READY') continue;
if (pollResp.data['status'] != 1) {
throw CaptchaException('Solve: ${pollResp.data['request']}');
}
return pollResp.data['request'];
}
throw CaptchaException('Timeout');
}
void dispose() => _dio.close();
}
Error Handling with Retry
Future<String> solveWithRetry(
CaptchaSolver solver,
CaptchaTask task, {
int maxRetries = 3,
}) async {
const retryable = ['ERROR_NO_SLOT_AVAILABLE', 'ERROR_CAPTCHA_UNSOLVABLE'];
CaptchaException? lastError;
for (var attempt = 0; attempt <= maxRetries; attempt++) {
if (attempt > 0) {
final delay = Duration(
milliseconds: (1000 * (1 << attempt)) + (DateTime.now().millisecond % 2000),
);
print('Retry $attempt/$maxRetries after ${delay.inSeconds}s');
await Future.delayed(delay);
}
try {
return await solver.solve(task);
} on CaptchaException catch (e) {
lastError = e;
final isRetryable = retryable.any((err) => e.message.contains(err));
if (!isRetryable) rethrow;
}
}
throw lastError ?? CaptchaException('Max retries exceeded');
}
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
ERROR_WRONG_USER_KEY |
Invalid API key | Verify key at dashboard |
ERROR_ZERO_BALANCE |
No funds | Top up account |
SocketException |
Network connectivity | Check internet connection |
FormatException |
Unexpected response | Check Content-Type, parse response body |
ClientException |
HTTP client error | Add timeout, check URL |
Isolate.run error |
Dart < 2.19 | Upgrade Dart SDK or use compute() |
FAQ
Does CaptchaAI have a Dart/Flutter package?
CaptchaAI provides a REST API. The examples here use Dart's standard http package — no special SDK needed.
Can I use this in a Flutter app?
Yes. Use the CaptchaService provider pattern shown above. Network calls run asynchronously and don't block the UI thread.
Does this work on Flutter Web?
Yes, but with limitations. The http package works on web, but CORS policies may require routing through a backend proxy.
How do I handle concurrency?
Use Future.wait() for light concurrency within a single isolate, or Isolate.run() for CPU-heavy parallel operations.
Related Guides
Add CAPTCHA solving to your Dart and Flutter apps — get your API key and integrate today.
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.