API Tutorials

PowerShell + CaptchaAI: solução de CAPTCHA de automação do Windows

Se um script PowerShell trava porque o formulário de teste exibe reCAPTCHA, Cloudflare Turnstile ou um CAPTCHA de imagem, a rota mais direta é chamar a API HTTP da CaptchaAI direto do Invoke-RestMethod — sem instalar nenhum módulo do PowerShell Gallery. Funciona tanto no PowerShell 5.1 nativo do Windows quanto no PowerShell 7+ multiplataforma, e cabe em qualquer pipeline que já rode .ps1.

Este guia mostra como montar funções de resolução para reCAPTCHA v2/v3, Cloudflare Turnstile e CAPTCHA de imagem, empacotar tudo em um módulo reutilizável e agendar a automação no Agendador de Tarefas do Windows — com retry, backoff exponencial e paralelismo via Start-Job.


Por que resolver CAPTCHA direto no PowerShell

  • Nativo do Windows — o PowerShell 5.1 já vem instalado; nenhuma dependência extra para baixar.
  • Invoke-RestMethod entende JSON sozinho — a resposta da API já chega como objeto, sem parsing manual.
  • Agendador de Tarefas — dá para rodar a automação em horário fixo, sem depender de um serviço externo.
  • Encaixa no pipeline — o token resolvido segue direto para o próximo passo do script.
  • PowerShell 7+ roda fora do Windows — o mesmo código funciona em runners Linux de CI/CD.

Cenário comum em QA de bancos e órgãos públicos que ainda operam Windows Server: o script roda às 6h pelo Agendador de Tarefas, preenche o formulário de homologação em staging.example.com e registra o resultado antes do expediente. Ao gravar logs dessa automação, considere a LGPD — prefira sempre dados fictícios em QA.

O que você precisa antes de começar

  • PowerShell 5.1 (Windows) ou PowerShell 7+ (multiplataforma)
  • Uma chave de API da CaptchaAI (crie a sua aqui)
  • Nenhum módulo adicional — os cmdlets usados já vêm no PowerShell

Funções base: enviar e consultar a tarefa

Enviar a tarefa

A função abaixo empacota a chamada POST para in.php: recebe a chave de API e os parâmetros da tarefa em uma hashtable, e lança uma exceção se o status retornado for diferente de 1.

function Submit-CaptchaTask {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [hashtable]$TaskParams
    )

    $body = @{
        key  = $ApiKey
        json = 1
    } + $TaskParams

    $response = Invoke-RestMethod -Uri "https://ocr.captchaai.com/in.php" `
        -Method Post `
        -Body $body `
        -ContentType "application/x-www-form-urlencoded"

    if ($response.status -ne 1) {
        throw "Submit failed: $($response.request)"
    }

    return $response.request
}

Consulta do resultado

Depois de enviar a tarefa, é preciso consultar res.php até a CaptchaAI devolver a solução. A função abaixo faz o polling em intervalo configurável e aplica um timeout — enquanto o request vier CAPCHA_NOT_READY, ela continua tentando.

function Get-CaptchaResult {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$TaskId,

        [int]$MaxWaitSeconds = 300,
        [int]$PollIntervalSeconds = 5
    )

    $deadline = (Get-Date).AddSeconds($MaxWaitSeconds)

    while ((Get-Date) -lt $deadline) {
        Start-Sleep -Seconds $PollIntervalSeconds

        $response = Invoke-RestMethod -Uri "https://ocr.captchaai.com/res.php" `
            -Method Get `
            -Body @{
                key    = $ApiKey
                action = "get"
                id     = $TaskId
                json   = 1
            }

        if ($response.request -eq "CAPCHA_NOT_READY") {
            Write-Verbose "Waiting for solution..."
            continue
        }

        if ($response.status -ne 1) {
            throw "Solve failed: $($response.request)"
        }

        return $response.request
    }

    throw "Timeout: CAPTCHA not solved within $MaxWaitSeconds seconds"
}

Resolvendo os principais tipos de CAPTCHA no PowerShell

Com Submit-CaptchaTask e Get-CaptchaResult prontas, cada tipo de CAPTCHA vira só uma combinação diferente de parâmetros na mesma dupla de funções.

reCAPTCHA v2

Monte o method: userrecaptcha com a sitekey (googlekey) e a URL da página (pageurl):

function Solve-RecaptchaV2 {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$SiteUrl,

        [Parameter(Mandatory)]
        [string]$SiteKey
    )

    Write-Host "Submitting reCAPTCHA v2 task..."
    $taskId = Submit-CaptchaTask -ApiKey $ApiKey -TaskParams @{
        method    = "userrecaptcha"
        googlekey = $SiteKey
        pageurl   = $SiteUrl
    }
    Write-Host "Task ID: $taskId"

    Write-Host "Polling for solution..."
    $token = Get-CaptchaResult -ApiKey $ApiKey -TaskId $taskId
    Write-Host "Solved! Token: $($token.Substring(0, [Math]::Min(50, $token.Length)))..."

    return $token
}

# Usage
$apiKey = "YOUR_API_KEY"
$token = Solve-RecaptchaV2 `
    -ApiKey $apiKey `
    -SiteUrl "https://staging.example.com/qa-login" `
    -SiteKey "6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-"

Cloudflare Turnstile

O fluxo segue a mesma estrutura, só trocando o method para turnstile:

function Solve-Turnstile {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$SiteUrl,

        [Parameter(Mandatory)]
        [string]$SiteKey
    )

    $taskId = Submit-CaptchaTask -ApiKey $ApiKey -TaskParams @{
        method  = "turnstile"
        key     = $SiteKey
        pageurl = $SiteUrl
    }

    return Get-CaptchaResult -ApiKey $ApiKey -TaskId $taskId
}

# Usage
$token = Solve-Turnstile `
    -ApiKey "YOUR_API_KEY" `
    -SiteUrl "https://example.com/form" `
    -SiteKey "0x4AAAAAAAB5..."

reCAPTCHA v3 (com score e action)

Informe a version e a action configurada no site — a CaptchaAI usa os dois para calcular o score correto:

function Solve-RecaptchaV3 {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$SiteUrl,

        [Parameter(Mandatory)]
        [string]$SiteKey,

        [string]$Action = "verify",
    )

    $taskId = Submit-CaptchaTask -ApiKey $ApiKey -TaskParams @{
        method    = "userrecaptcha"
        googlekey = $SiteKey
        pageurl   = $SiteUrl
        version   = "v3"
        action    = $Action
    }

    return Get-CaptchaResult -ApiKey $ApiKey -TaskId $taskId
}

Resolvendo CAPTCHA de imagem

A partir de um arquivo local

Para CAPTCHA de imagem, a CaptchaAI espera o conteúdo em base64 no parâmetro body, com method: base64. A função lê o arquivo, converte e reaproveita o par enviar/consultar já criado:

function Solve-ImageCaptcha {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$ImagePath
    )

    if (-not (Test-Path $ImagePath)) {
        throw "Image file not found: $ImagePath"
    }

    $imageBytes = [System.IO.File]::ReadAllBytes($ImagePath)
    $base64 = [Convert]::ToBase64String($imageBytes)

    $taskId = Submit-CaptchaTask -ApiKey $ApiKey -TaskParams @{
        method = "base64"
        body   = $base64
    }

    return Get-CaptchaResult -ApiKey $ApiKey -TaskId $taskId
}

# Usage
$text = Solve-ImageCaptcha -ApiKey "YOUR_API_KEY" -ImagePath "C:\captcha.png"
Write-Host "CAPTCHA text: $text"

A partir de uma URL

Quando a imagem já está hospedada, dá para baixar o conteúdo com Invoke-WebRequest e pular a etapa de gravar em disco:

function Solve-ImageCaptchaFromUrl {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [string]$ImageUrl
    )

    $imageBytes = (Invoke-WebRequest -Uri $ImageUrl).Content
    $base64 = [Convert]::ToBase64String($imageBytes)

    $taskId = Submit-CaptchaTask -ApiKey $ApiKey -TaskParams @{
        method = "base64"
        body   = $base64
    }

    return Get-CaptchaResult -ApiKey $ApiKey -TaskId $taskId
}

Um módulo completo para reaproveitar em outros scripts

Para não copiar as mesmas funções em cada script, vale empacotar tudo em uma classe CaptchaAISolver e salvar como CaptchaAI.psm1. Ela cobre reCAPTCHA v2, Turnstile e CAPTCHA de imagem, e ainda expõe GetBalance() para checar o saldo antes de disparar um lote grande de tarefas:

class CaptchaAISolver {
    [string]$ApiKey
    [string]$BaseUrl = "https://ocr.captchaai.com"
    [int]$PollInterval = 5
    [int]$MaxWait = 300

    CaptchaAISolver([string]$apiKey) {
        $this.ApiKey = $apiKey
    }

    [string] SolveRecaptchaV2([string]$siteUrl, [string]$siteKey) {
        return $this.Solve(@{
            method    = "userrecaptcha"
            googlekey = $siteKey
            pageurl   = $siteUrl
        })
    }

    [string] SolveTurnstile([string]$siteUrl, [string]$siteKey) {
        return $this.Solve(@{
            method  = "turnstile"
            key     = $siteKey
            pageurl = $siteUrl
        })
    }

    [string] SolveImage([string]$imagePath) {
        $bytes = [System.IO.File]::ReadAllBytes($imagePath)
        $base64 = [Convert]::ToBase64String($bytes)
        return $this.Solve(@{
            method = "base64"
            body   = $base64
        })
    }

    [double] GetBalance() {
        $response = Invoke-RestMethod -Uri "$($this.BaseUrl)/res.php" `
            -Body @{ key = $this.ApiKey; action = "getbalance"; json = 1 }
        return [double]$response.request
    }

    hidden [string] Solve([hashtable]$params) {
        $taskId = $this.Submit($params)
        return $this.Poll($taskId)
    }

    hidden [string] Submit([hashtable]$params) {
        $body = @{ key = $this.ApiKey; json = 1 } + $params
        $response = Invoke-RestMethod -Uri "$($this.BaseUrl)/in.php" `
            -Method Post -Body $body
        if ($response.status -ne 1) { throw "Submit: $($response.request)" }
        return $response.request
    }

    hidden [string] Poll([string]$taskId) {
        $deadline = (Get-Date).AddSeconds($this.MaxWait)
        while ((Get-Date) -lt $deadline) {
            Start-Sleep -Seconds $this.PollInterval
            $response = Invoke-RestMethod -Uri "$($this.BaseUrl)/res.php" `
                -Body @{ key = $this.ApiKey; action = "get"; id = $taskId; json = 1 }
            if ($response.request -eq "CAPCHA_NOT_READY") { continue }
            if ($response.status -ne 1) { throw "Solve: $($response.request)" }
            return $response.request
        }
        throw "Timeout"
    }
}

# Export
Export-ModuleMember

Como importar e usar o módulo

Depois de salvo, basta importar com using module e instanciar a classe com a chave de API:

using module .\CaptchaAI.psm1

$solver = [CaptchaAISolver]::new("YOUR_API_KEY")

# Check balance
$balance = $solver.GetBalance()
Write-Host "Balance: `$$balance"

# Solve reCAPTCHA v2
$token = $solver.SolveRecaptchaV2("https://staging.example.com/qa-login", "SITEKEY")
Write-Host "Token: $($token.Substring(0, 50))..."

Colocando a automação em produção

Enviando o formulário com o token resolvido

O token retornado pela CaptchaAI entra no campo g-recaptcha-response do formulário de destino. Para Turnstile, troque o nome do campo pelo esperado no site (cf-turnstile-response):

function Submit-FormWithToken {
    param(
        [string]$Url,
        [string]$Token,
        [hashtable]$FormData
    )

    $body = $FormData + @{
        "g-recaptcha-response" = $Token
    }

    $response = Invoke-WebRequest -Uri $Url `
        -Method Post `
        -Body $body `
        -ContentType "application/x-www-form-urlencoded"

    return $response
}

# Usage
$token = Solve-RecaptchaV2 -ApiKey "YOUR_API_KEY" `
    -SiteUrl "https://staging.example.com/qa-login" `
    -SiteKey "SITEKEY"

$result = Submit-FormWithToken `
    -Url "https://staging.example.com/qa-login" `
    -Token $token `
    -FormData @{
        username = "user@example.com"
        password = "password"
    }

Write-Host "Response: $($result.StatusCode)"

Resolvendo vários CAPTCHAs em paralelo com Start-Job

Start-Job evita que cada Invoke-RestMethod bloqueie o script inteiro enquanto espera o polling. O paralelismo real depende do plano: o BASIC (US$ 15/mês) libera 5 threads simultâneas, o STANDARD (US$ 30/mês) sobe para 15 — o número de Start-Job ativos deve respeitar esse limite.

$apiKey = "YOUR_API_KEY"

$tasks = @(
    @{ Url = "https://site-a.com"; Key = "SITEKEY_A" },
    @{ Url = "https://site-b.com"; Key = "SITEKEY_B" },
    @{ Url = "https://site-c.com"; Key = "SITEKEY_C" }
)

$jobs = $tasks | ForEach-Object {
    $task = $_
    Start-Job -ScriptBlock {
        param($ApiKey, $Url, $SiteKey)

        $taskId = (Invoke-RestMethod -Uri "https://ocr.captchaai.com/in.php" -Method Post -Body @{
            key = $ApiKey; json = 1; method = "userrecaptcha"
            googlekey = $SiteKey; pageurl = $Url
        }).request

        $deadline = (Get-Date).AddSeconds(300)
        while ((Get-Date) -lt $deadline) {
            Start-Sleep -Seconds 5
            $result = Invoke-RestMethod -Uri "https://ocr.captchaai.com/res.php" -Body @{
                key = $ApiKey; action = "get"; id = $taskId; json = 1
            }
            if ($result.request -ne "CAPCHA_NOT_READY" -and $result.status -eq 1) {
                return @{ Url = $Url; Token = $result.request }
            }
        }
        return @{ Url = $Url; Error = "Timeout" }
    } -ArgumentList $apiKey, $task.Url, $task.Key
}

# Wait and collect results
$results = $jobs | Wait-Job | Receive-Job
$results | ForEach-Object {
    if ($_.Token) {
        Write-Host "$($_.Url): $($_.Token.Substring(0, 50))..."
    } else {
        Write-Host "$($_.Url): $($_.Error)" -ForegroundColor Red
    }
}
$jobs | Remove-Job

Retry com backoff exponencial

Nem todo erro deve gerar nova tentativa: a função abaixo só reenvia para ERROR_NO_SLOT_AVAILABLE e ERROR_CAPTCHA_UNSOLVABLE, com backoff exponencial e jitter para não sobrecarregar a API:

function Solve-WithRetry {
    param(
        [Parameter(Mandatory)]
        [string]$ApiKey,

        [Parameter(Mandatory)]
        [hashtable]$TaskParams,

        [int]$MaxRetries = 3
    )

    $retryableErrors = @(
        "ERROR_NO_SLOT_AVAILABLE",
        "ERROR_CAPTCHA_UNSOLVABLE"
    )

    for ($attempt = 0; $attempt -le $MaxRetries; $attempt++) {
        if ($attempt -gt 0) {
            $delay = [Math]::Pow(2, $attempt) + (Get-Random -Maximum 3)
            Write-Host "Retry $attempt/$MaxRetries after $($delay)s..."
            Start-Sleep -Seconds $delay
        }

        try {
            $taskId = Submit-CaptchaTask -ApiKey $ApiKey -TaskParams $TaskParams
            $result = Get-CaptchaResult -ApiKey $ApiKey -TaskId $taskId
            return $result
        }
        catch {
            $errorMsg = $_.Exception.Message
            $isRetryable = $retryableErrors | Where-Object { $errorMsg -like "*$_*" }

            if (-not $isRetryable -or $attempt -eq $MaxRetries) {
                throw
            }
            Write-Warning "Retryable error: $errorMsg"
        }
    }
}

Agendando a automação no Agendador de Tarefas do Windows

Para rodar a automação sem intervenção manual, registre um ScheduledTask que dispara o script todo dia no mesmo horário. Guarde a chave de API fora do script — em uma variável de ambiente da máquina ou em um cofre de segredos — em vez de deixá-la em texto puro dentro do .ps1 que fica no disco:

# Create a scheduled task that runs CAPTCHA automation daily
$action = New-ScheduledTaskAction `
    -Execute "powershell.exe" `
    -Argument "-ExecutionPolicy validação autorizada -File C:\Scripts\captcha-automation.ps1"

$trigger = New-ScheduledTaskTrigger -Daily -At "08:00"

Register-ScheduledTask `
    -TaskName "CaptchaAutomation" `
    -Action $action `
    -Trigger $trigger `
    -Description "Run daily CAPTCHA automation with CaptchaAI"

Erros comuns e como resolver

Erros mais frequentes ao integrar a API no PowerShell:

Erro Causa Correção
ERROR_WRONG_USER_KEY Chave de API inválida ou copiada errada Confira a chave no painel da CaptchaAI
ERROR_ZERO_BALANCE Saldo zerado na conta Faça a recarga de créditos
Invoke-RestMethod: SSL/TLS Versão de TLS incompatível Adicione [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
The response content cannot be parsed A resposta não veio em JSON Use Invoke-WebRequest e faça o parsing manualmente
Erro de Execution policy Script bloqueado pela política de execução Execute Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
Cannot convert to double Falha ao converter o saldo retornado Use [double]::Parse($response.request)

Perguntas frequentes

Qual plano da CaptchaAI faz sentido para rodar vários scripts em paralelo?

Depende de quantos Start-Job ficam ativos ao mesmo tempo: o BASIC (US$ 15/mês) inclui 5 threads, o STANDARD (US$ 30/mês) sobe para 15 e o ADVANCE (US$ 90/mês) chega a 50 — todos com resoluções ilimitadas por thread.

Não. Invoke-RestMethod e Invoke-WebRequest já vêm no PowerShell 5.1 e no 7+; a API é HTTP puro, sem SDK obrigatório.

O Agendador de Tarefas executa o script, mas o CAPTCHA nunca é resolvido — o que verificar?

Confira se a tarefa usa a mesma versão do PowerShell dos testes manuais e se a Set-ExecutionPolicy permite o script naquele contexto de usuário — sessões agendadas costumam rodar sem perfil carregado, então caminhos relativos podem falhar.

Por que o Invoke-RestMethod retorna erro de SSL/TLS ao chamar a API?

Geralmente porque o Windows ainda usa um protocolo TLS mais antigo. Adicione [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 antes da primeira chamada.

Como evito deixar a chave de API exposta em texto puro no script agendado?

Leia a chave de uma variável de ambiente ($env:CAPTCHAAI_KEY) ou de um cofre como o Windows Credential Manager, em vez de gravá-la direto no .ps1.


Guias relacionados


Automatize CAPTCHAs a partir da linha de comando do Windows — pegue sua chave de API e comece a automatizar em minutos.

Os comentários estão desativados para este artigo.