API Tutorials

Solving CAPTCHAs with Perl and CaptchaAI API

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

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.



Extend your Perl scripts with CAPTCHA solving — get your API key and integrate today.

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.