Perl remains widely used for system administration, text processing, web scraping, and legacy application maintenance. When Perl scripts encounter CAPTCHAs on web forms or data portals, CaptchaAI's HTTP API integrates through Perl's battle-tested HTTP modules — LWP, HTTP::Tiny, or Mojo::UserAgent.
This guide covers reCAPTCHA v2/v3, Cloudflare Turnstile, and image CAPTCHA solving with production-ready Perl modules and scripts.
Why Perl for CAPTCHA Automation
- Ubiquitous — installed on virtually every Unix/Linux system
- Regex powerhouse — extract sitekeys and parse HTML with unmatched regex support
- CPAN ecosystem — thousands of modules for HTTP, JSON, HTML parsing
- Text processing — ideal for scraping and data extraction pipelines
- Legacy integration — extend existing Perl applications with CAPTCHA support
Prerequisites
# Core modules (most are included with Perl)
cpan install LWP::UserAgent
cpan install JSON
cpan install MIME::Base64
cpan install URI::Escape
# Optional
cpan install HTTP::Tiny # lightweight alternative
cpan install Mojo::UserAgent # async alternative
cpan install HTML::TreeBuilder # HTML parsing
- Perl 5.20+
- CaptchaAI API key (get one here)
Method 1: LWP::UserAgent (Standard)
CaptchaSolver Module
package CaptchaSolver;
use strict;
use warnings;
use LWP::UserAgent;
use JSON qw(decode_json);
use URI;
use Carp qw(croak);
sub new {
my ($class, %args) = @_;
croak "api_key required" unless $args{api_key};
my $self = bless {
api_key => $args{api_key},
base_url => 'https://ocr.captchaai.com',
poll_interval => $args{poll_interval} || 5,
max_wait => $args{max_wait} || 300,
ua => LWP::UserAgent->new(
timeout => 30,
agent => 'CaptchaSolver-Perl/1.0',
),
}, $class;
return $self;
}
sub solve_recaptcha_v2 {
my ($self, %args) = @_;
croak "site_url required" unless $args{site_url};
croak "sitekey required" unless $args{sitekey};
my $task_id = $self->_submit(
method => 'userrecaptcha',
googlekey => $args{sitekey},
pageurl => $args{site_url},
);
return $self->_poll($task_id);
}
sub solve_recaptcha_v3 {
my ($self, %args) = @_;
my $task_id = $self->_submit(
method => 'userrecaptcha',
googlekey => $args{sitekey},
pageurl => $args{site_url},
version => 'v3',
action => $args{action} || 'verify',
min_score => $args{min_score} || 0.7,
);
return $self->_poll($task_id);
}
sub solve_turnstile {
my ($self, %args) = @_;
my $task_id = $self->_submit(
method => 'turnstile',
key => $args{sitekey},
pageurl => $args{site_url},
);
return $self->_poll($task_id);
}
sub solve_image {
my ($self, %args) = @_;
my $base64;
if ($args{file}) {
open my $fh, '<:raw', $args{file}
or croak "Cannot open $args{file}: $!";
local $/;
my $data = <$fh>;
close $fh;
require MIME::Base64;
$base64 = MIME::Base64::encode_base64($data, '');
} elsif ($args{base64}) {
$base64 = $args{base64};
} else {
croak "file or base64 required";
}
my $task_id = $self->_submit(
method => 'base64',
body => $base64,
);
return $self->_poll($task_id);
}
sub check_balance {
my ($self) = @_;
my $uri = URI->new("$self->{base_url}/res.php");
$uri->query_form(
key => $self->{api_key},
action => 'getbalance',
json => 1,
);
my $response = $self->{ua}->get($uri);
croak "HTTP error: " . $response->status_line unless $response->is_success;
my $data = decode_json($response->decoded_content);
return $data->{request} + 0;
}
sub _submit {
my ($self, %params) = @_;
$params{key} = $self->{api_key};
$params{json} = 1;
my $response = $self->{ua}->post(
"$self->{base_url}/in.php",
Content => \%params,
);
croak "HTTP error: " . $response->status_line unless $response->is_success;
my $data = decode_json($response->decoded_content);
croak "Submit failed: $data->{request}" unless $data->{status} == 1;
return $data->{request};
}
sub _poll {
my ($self, $task_id) = @_;
my $elapsed = 0;
while ($elapsed < $self->{max_wait}) {
sleep $self->{poll_interval};
$elapsed += $self->{poll_interval};
my $uri = URI->new("$self->{base_url}/res.php");
$uri->query_form(
key => $self->{api_key},
action => 'get',
id => $task_id,
json => 1,
);
my $response = $self->{ua}->get($uri);
next unless $response->is_success;
my $data = decode_json($response->decoded_content);
next if $data->{request} eq 'CAPCHA_NOT_READY';
croak "Solve failed: $data->{request}" unless $data->{status} == 1;
return $data->{request};
}
croak "Timeout: CAPTCHA not solved within $self->{max_wait} seconds";
}
1;
Usage
#!/usr/bin/perl
use strict;
use warnings;
use lib '.';
use CaptchaSolver;
my $solver = CaptchaSolver->new(api_key => 'YOUR_API_KEY');
# Check balance
my $balance = $solver->check_balance();
printf "Balance: \$%.2f\n", $balance;
# Solve reCAPTCHA v2
my $token = $solver->solve_recaptcha_v2(
site_url => 'https://example.com/login',
sitekey => '6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-',
);
printf "Token: %.50s...\n", $token;
# Solve Turnstile
my $turnstile = $solver->solve_turnstile(
site_url => 'https://example.com/form',
sitekey => '0x4AAAAAAAB5...',
);
printf "Turnstile: %.50s...\n", $turnstile;
# Solve image
my $text = $solver->solve_image(file => 'captcha.png');
print "Image text: $text\n";
Method 2: HTTP::Tiny (Lightweight)
use strict;
use warnings;
use HTTP::Tiny;
use JSON qw(decode_json);
use URI::Escape qw(uri_escape);
my $http = HTTP::Tiny->new(timeout => 30);
my $api_key = 'YOUR_API_KEY';
my $base_url = 'https://ocr.captchaai.com';
sub solve_recaptcha_v2_tiny {
my ($site_url, $sitekey) = @_;
# Submit
my $body = join '&',
"key=$api_key",
"json=1",
"method=userrecaptcha",
"googlekey=" . uri_escape($sitekey),
"pageurl=" . uri_escape($site_url);
my $resp = $http->request('POST', "$base_url/in.php", {
content => $body,
headers => { 'content-type' => 'application/x-www-form-urlencoded' },
});
die "Submit HTTP error: $resp->{status}" unless $resp->{success};
my $data = decode_json($resp->{content});
die "Submit: $data->{request}" unless $data->{status} == 1;
my $task_id = $data->{request};
# Poll
for (1..60) {
sleep 5;
my $poll = $http->get(
"$base_url/res.php?key=$api_key&action=get&id=$task_id&json=1"
);
next unless $poll->{success};
my $result = decode_json($poll->{content});
next if $result->{request} eq 'CAPCHA_NOT_READY';
die "Solve: $result->{request}" unless $result->{status} == 1;
return $result->{request};
}
die "Timeout";
}
my $token = solve_recaptcha_v2_tiny(
'https://example.com/login',
'6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-'
);
print "Token: $token\n";
Sitekey Extraction
use HTML::TreeBuilder;
use LWP::UserAgent;
sub extract_sitekey {
my ($url) = @_;
my $ua = LWP::UserAgent->new();
my $response = $ua->get($url);
die "Fetch failed" unless $response->is_success;
my $tree = HTML::TreeBuilder->new_from_content($response->decoded_content);
# reCAPTCHA
my @recaptcha = $tree->look_down('data-sitekey', qr/.+/);
if (@recaptcha) {
return {
type => 'recaptcha',
key => $recaptcha[0]->attr('data-sitekey'),
};
}
# Turnstile
my @turnstile = $tree->look_down(class => qr/cf-turnstile/, 'data-sitekey', qr/.+/);
if (@turnstile) {
return {
type => 'turnstile',
key => $turnstile[0]->attr('data-sitekey'),
};
}
# Script regex fallback
my $html = $response->decoded_content;
if ($html =~ /sitekey['":\s]+['"]([A-Za-z0-9_-]{20,})['"]/) {
return { type => 'unknown', key => $1 };
}
return undef;
}
Submitting Forms with Solved Tokens
sub submit_form_with_token {
my ($url, $token, %form_data) = @_;
$form_data{'g-recaptcha-response'} = $token;
my $ua = LWP::UserAgent->new();
my $response = $ua->post($url, \%form_data);
return {
code => $response->code,
content => $response->decoded_content,
success => $response->is_success,
};
}
# Usage
my $token = $solver->solve_recaptcha_v2(
site_url => 'https://example.com/login',
sitekey => 'SITEKEY',
);
my $result = submit_form_with_token(
'https://example.com/login',
$token,
username => 'user@example.com',
password => 'password',
);
print "Login: $result->{code}\n";
Parallel Solving with Threads
use threads;
use threads::shared;
my @results :shared;
my $solver = CaptchaSolver->new(api_key => 'YOUR_API_KEY');
my @tasks = (
{ url => 'https://site-a.com', key => 'KEY_A' },
{ url => 'https://site-b.com', key => 'KEY_B' },
{ url => 'https://site-c.com', key => 'KEY_C' },
);
my @threads;
for my $task (@tasks) {
push @threads, threads->create(sub {
my $t = shift;
eval {
my $s = CaptchaSolver->new(api_key => 'YOUR_API_KEY');
my $token = $s->solve_recaptcha_v2(
site_url => $t->{url},
sitekey => $t->{key},
);
lock(@results);
push @results, "$t->{url}: " . substr($token, 0, 50) . "...";
};
if ($@) {
lock(@results);
push @results, "$t->{url}: ERROR: $@";
}
}, $task);
}
$_->join() for @threads;
print "$_\n" for @results;
Error Handling with Retry
sub solve_with_retry {
my ($solver, %args) = @_;
my $max_retries = delete $args{max_retries} || 3;
my $type = delete $args{type} || 'recaptcha_v2';
my @retryable = qw(
ERROR_NO_SLOT_AVAILABLE
ERROR_CAPTCHA_UNSOLVABLE
);
for my $attempt (0 .. $max_retries) {
if ($attempt > 0) {
my $delay = 2**$attempt + int(rand(3));
warn "Retry $attempt/$max_retries after ${delay}s\n";
sleep $delay;
}
my $token = eval {
if ($type eq 'turnstile') {
return $solver->solve_turnstile(%args);
} else {
return $solver->solve_recaptcha_v2(%args);
}
};
return $token unless $@;
my $error = $@;
my $is_retryable = grep { $error =~ /\Q$_\E/ } @retryable;
die $error unless $is_retryable;
}
die "Max retries exceeded";
}
# Usage
my $token = solve_with_retry(
$solver,
type => 'recaptcha_v2',
site_url => 'https://example.com',
sitekey => 'SITEKEY',
max_retries => 3,
);
One-Liner for Quick Solves
# Solve reCAPTCHA v2 from the command line
perl -MLWP::UserAgent -MJSON -e '
my $ua = LWP::UserAgent->new;
my $key = $ENV{CAPTCHAAI_KEY};
my $r = $ua->post("https://ocr.captchaai.com/in.php", {
key => $key, json => 1, method => "userrecaptcha",
googlekey => $ARGV[0], pageurl => $ARGV[1]
});
my $tid = decode_json($r->content)->{request};
for (1..60) { sleep 5;
my $p = $ua->get("https://ocr.captchaai.com/res.php?key=$key&action=get&id=$tid&json=1");
my $d = decode_json($p->content);
next if $d->{request} eq "CAPCHA_NOT_READY";
print $d->{request}; last;
}
' "SITEKEY" "https://example.com"
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
ERROR_WRONG_USER_KEY |
Invalid API key | Verify key at dashboard |
ERROR_ZERO_BALANCE |
No funds | Top up account |
Can't locate LWP/UserAgent.pm |
Module not installed | cpan install LWP::UserAgent |
SSL connect attempt failed |
SSL cert issue | Install LWP::Protocol::https and Mozilla::CA |
500 Can't connect |
Network/DNS issue | Check connectivity |
Malformed JSON |
Non-JSON response | Check URL and API status |
FAQ
Does CaptchaAI have a Perl module on CPAN?
CaptchaAI uses a REST API. The module shown here works with standard CPAN HTTP libraries — no special SDK needed.
Which HTTP module should I use?
Use LWP::UserAgent for full-featured scripts, HTTP::Tiny for lightweight scripts with no non-core dependencies, or Mojo::UserAgent for async workflows.
Can I use this with CGI or Dancer2?
Yes. Wrap the solver in a service class and call it from your web application's controller.
Does Perl threading work for parallel solves?
Perl threads work but are heavyweight. For parallel solving, consider forking with Parallel::ForkManager or using async I/O with Mojo::IOLoop.
Related Guides
Extend your Perl scripts with CAPTCHA solving — get your API key and integrate today.
Discussions (0)
Join the conversation
Sign in to share your opinion.
Sign InNo comments yet.