Um sitekey vazio ou um pageurl sem https:// só aparecem como erro depois que a requisição já foi e voltou da API da CaptchaAI — normalmente uns 5 segundos perdidos por chamada, multiplicados por cada worker do pipeline que repete o mesmo parâmetro quebrado. A saída mais simples é validar antes de sair da máquina local: com Pydantic v2, um sitekey curto demais ou uma URL sem esquema disparam um ValidationError na hora, sem gastar uma única requisição HTTP.
Este guia monta um cliente Python completo para a API da CaptchaAI. Você vai sair daqui com:
- modelos Pydantic de requisição para reCAPTCHA v2, reCAPTCHA v3, Cloudflare Turnstile e CAPTCHA de imagem;
- modelos de resposta que transformam o JSON da API em objetos tipados, sem
dict["key"]solto pelo código; - uma classe
CaptchaAIcompleta, com submissão, polling e tratamento de erro; - uma tabela de erros comuns e cinco perguntas frequentes sobre validação, migração de versão e uso assíncrono.
Por que validar antes de chamar a API
Em um pipeline de QA rodando em sa-east-1 (São Paulo), por exemplo, um erro de parâmetro só é descoberto no fim da fila de polling — o worker já reservou uma thread do seu plano CaptchaAI, esperou o poll_interval e só então recebeu ERROR_WRONG_CAPTCHA_ID. Validar o payload antes do POST corta esse ciclo pela raiz.
Efeito colateral útil: como a validação acontece antes de qualquer chamada de rede, um
sitekeyoupageurlmalformado nunca chega a ser enviado para fora da sua máquina — o que também ajuda equipes que seguem a LGPD e tratam logs de teste como dado a ser minimizado.
O ganho na prática, comparando os dois cenários:
- Sem Pydantic: sitekey vazio só vira erro depois de uma chamada de API completa (uns 5 s de espera); parsing de resposta via
dict["key"]quebra comKeyError; o editor não sugere nada sobre os parâmetros esperados. - Com Pydantic: o mesmo sitekey vazio dispara
ValidationErrorna hora, antes dorequests.post; a resposta vira um modelo tipado com validação embutida; o IDE autocompleta cada campo.
Modelos Pydantic por tipo de CAPTCHA
Cada tipo suportado pela CaptchaAI ganha seu próprio modelo de requisição:
RecaptchaV2Request— sitekey, pageurl e a flaginvisible;RecaptchaV3Request— adicionaactione oscore_qaesperado;TurnstileRequest— sitekey, pageurl e ocdataopcional do Cloudflare Turnstile;ImageRequest— imagem em base64, com umfield_validatorque já remove o prefixodata:se ele vier junto.
Do outro lado, os modelos de resposta (SubmitResponse, PollResponse, SolveResult) fazem o caminho inverso: pegam o JSON solto que a API devolve e expõem propriedades já tipadas, então result.token chega pronto para uso em vez de um dict["request"] que pode nem existir.
# models.py
from pydantic import BaseModel, Field, field_validator, HttpUrl
from enum import Enum
from typing import Optional
class CaptchaMethod(str, Enum):
RECAPTCHA_V2 = "userrecaptcha"
RECAPTCHA_V3 = "userrecaptcha" # Differentiated by version field
TURNSTILE = "turnstile"
HCAPTCHA = "hcaptcha"
IMAGE = "base64"
GEETEST = "geetest"
class RecaptchaV2Request(BaseModel):
"""Parameters for solving reCAPTCHA v2."""
sitekey: str = Field(min_length=20, max_length=100, description="Site's reCAPTCHA sitekey")
pageurl: HttpUrl = Field(description="URL where CAPTCHA appears")
invisible: bool = False
cookies: Optional[str] = None
@field_validator("sitekey")
@classmethod
def validate_sitekey(cls, v: str) -> str:
if v.strip() != v:
raise ValueError("Sitekey must not have leading/trailing whitespace")
return v
def to_params(self) -> dict:
params = {
"method": "userrecaptcha",
"googlekey": self.sitekey,
"pageurl": str(self.pageurl),
}
if self.invisible:
params["invisible"] = "1"
if self.cookies:
params["cookies"] = self.cookies
return params
class RecaptchaV3Request(BaseModel):
"""Parameters for solving reCAPTCHA v3."""
sitekey: str = Field(min_length=20, max_length=100)
pageurl: HttpUrl
action: str = Field(default="verify", min_length=1, max_length=100)
score_qa: float = Field(default=0.3, ge=0.1, le=0.9)
def to_params(self) -> dict:
return {
"method": "userrecaptcha",
"version": "v3",
"googlekey": self.sitekey,
"pageurl": str(self.pageurl),
"action": self.action,
"score_qa": str(self.score_qa),
}
class TurnstileRequest(BaseModel):
"""Parameters for solving Cloudflare Turnstile."""
sitekey: str = Field(min_length=10, max_length=100)
pageurl: HttpUrl
action: Optional[str] = None
cdata: Optional[str] = None
def to_params(self) -> dict:
params = {
"method": "turnstile",
"sitekey": self.sitekey,
"pageurl": str(self.pageurl),
}
if self.action:
params["action"] = self.action
if self.cdata:
params["data"] = self.cdata
return params
class ImageRequest(BaseModel):
"""Parameters for solving image/text CAPTCHA."""
base64_image: str = Field(min_length=100, description="Base64-encoded image")
case_sensitive: bool = False
min_length: Optional[int] = Field(default=None, ge=1, le=50)
max_length: Optional[int] = Field(default=None, ge=1, le=50)
@field_validator("base64_image")
@classmethod
def validate_base64(cls, v: str) -> str:
# Strip data URI prefix if present
if v.startswith("data:"):
parts = v.split(",", 1)
if len(parts) == 2:
return parts[1]
return v
def to_params(self) -> dict:
params = {
"method": "base64",
"body": self.base64_image,
}
if self.case_sensitive:
params["regsense"] = "1"
if self.min_length is not None:
params["min_len"] = str(self.min_length)
if self.max_length is not None:
params["max_len"] = str(self.max_length)
return params
class SubmitResponse(BaseModel):
"""Parsed API submit response."""
status: int
request: str
@property
def success(self) -> bool:
return self.status == 1
@property
def task_id(self) -> str:
if not self.success:
raise ValueError(f"No task ID — submission failed: {self.request}")
return self.request
class PollResponse(BaseModel):
"""Parsed API poll response."""
status: int
request: str
@property
def ready(self) -> bool:
return self.request != "CAPCHA_NOT_READY"
@property
def success(self) -> bool:
return self.status == 1
@property
def token(self) -> str:
if not self.success:
raise ValueError(f"No token — solve failed: {self.request}")
return self.request
class SolveResult(BaseModel):
"""Result of a successful solve."""
token: str
task_id: str
solve_time: float = Field(description="Solve time in seconds")
Cliente que valida antes de enviar
A classe CaptchaAI reúne submissão (_submit), polling (_poll) e conversão para SolveResult em um único ponto — cada método público (solve_recaptcha_v2, solve_turnstile, solve_image...) recebe os parâmetros crus, instancia o modelo Pydantic correspondente e só então monta os parâmetros da requisição com to_params(). Se a validação falhar, o ValidationError sobe direto para quem chamou, antes de qualquer requests.post para in.php. O polling em _poll usa time.monotonic() para respeitar o timeout configurado e evita ficar preso indefinidamente em uma tarefa que nunca fica pronta.
# client.py
import time
import requests
from pydantic import ValidationError
from models import (
RecaptchaV2Request,
RecaptchaV3Request,
TurnstileRequest,
ImageRequest,
SubmitResponse,
PollResponse,
SolveResult,
)
SUBMIT_URL = "https://ocr.captchaai.com/in.php"
RESULT_URL = "https://ocr.captchaai.com/res.php"
class CaptchaAIError(Exception):
def __init__(self, code: str, message: str = ""):
self.code = code
super().__init__(f"{code}: {message}" if message else code)
class CaptchaAI:
def __init__(self, api_key: str, poll_interval: int = 5, timeout: int = 180):
if not api_key or len(api_key) < 10:
raise ValueError("Invalid API key")
self.api_key = api_key
self.poll_interval = poll_interval
self.timeout = timeout
def _submit(self, params: dict) -> str:
params["key"] = self.api_key
params["json"] = 1
resp = requests.post(SUBMIT_URL, data=params, timeout=30)
result = SubmitResponse.model_validate(resp.json())
if not result.success:
raise CaptchaAIError(result.request, "Submit failed")
return result.task_id
def _poll(self, task_id: str) -> str:
start = time.monotonic()
while time.monotonic() - start < self.timeout:
time.sleep(self.poll_interval)
resp = requests.get(RESULT_URL, params={
"key": self.api_key,
"action": "get",
"id": task_id,
"json": 1,
}, timeout=15)
result = PollResponse.model_validate(resp.json())
if not result.ready:
continue
if result.success:
return result.token
raise CaptchaAIError(result.request, "Solve failed")
raise CaptchaAIError("TIMEOUT", f"Task {task_id} timed out after {self.timeout}s")
def _solve(self, params: dict) -> SolveResult:
start = time.monotonic()
task_id = self._submit(params)
token = self._poll(task_id)
elapsed = time.monotonic() - start
return SolveResult(
token=token,
task_id=task_id,
solve_time=round(elapsed, 1),
)
def solve_recaptcha_v2(self, sitekey: str, pageurl: str, **kwargs) -> SolveResult:
"""Solve reCAPTCHA v2 with validated parameters."""
req = RecaptchaV2Request(sitekey=sitekey, pageurl=pageurl, **kwargs)
return self._solve(req.to_params())
def solve_recaptcha_v3(self, sitekey: str, pageurl: str, **kwargs) -> SolveResult:
"""Solve reCAPTCHA v3 with validated parameters."""
req = RecaptchaV3Request(sitekey=sitekey, pageurl=pageurl, **kwargs)
return self._solve(req.to_params())
def solve_turnstile(self, sitekey: str, pageurl: str, **kwargs) -> SolveResult:
"""Solve Cloudflare Turnstile with validated parameters."""
req = TurnstileRequest(sitekey=sitekey, pageurl=pageurl, **kwargs)
return self._solve(req.to_params())
def solve_image(self, base64_image: str, **kwargs) -> SolveResult:
"""Solve image/text CAPTCHA with validated parameters."""
req = ImageRequest(base64_image=base64_image, **kwargs)
return self._solve(req.to_params())
def get_balance(self) -> float:
"""Get current account balance."""
resp = requests.get(RESULT_URL, params={
"key": self.api_key,
"action": "getbalance",
"json": 1,
}, timeout=10)
result = SubmitResponse.model_validate(resp.json())
return float(result.request)
Exemplo: requisição válida e três formas de falhar cedo
O trecho abaixo usa staging.example.com como URL de teste — troque pelo domínio do seu próprio ambiente de QA. Repare que os três try/except capturam falhas em pontos diferentes: sitekey inválido nunca sai da máquina local, score_qa fora do intervalo também é barrado antes do envio, e só o erro de CaptchaAIError (saldo, chave errada etc.) depende de uma resposta real da API.
from pydantic import ValidationError
from client import CaptchaAI, CaptchaAIError
client = CaptchaAI("YOUR_API_KEY", timeout=120)
# Valid request — passes validation, calls API
result = client.solve_recaptcha_v2(
sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl="https://staging.example.com/qa-login",
)
print(f"Token: {result.token[:40]}...")
print(f"Solved in {result.solve_time}s")
# Invalid sitekey — caught immediately, no API call
try:
client.solve_recaptcha_v2(sitekey="", pageurl="https://example.com")
except ValidationError as e:
print(e)
# sitekey: String should have at least 20 characters
# Invalid score — caught before API call
try:
client.solve_recaptcha_v3(
sitekey="6Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-",
pageurl="https://example.com",
score_qa=1.5, # Invalid — max is 0.9
)
except ValidationError as e:
print(e)
# score_qa: Input should be less than or equal to 0.9
# API error — caught during request
try:
result = client.solve_turnstile(
sitekey="0x4AAAAAAADnPIDROrmt1Wwj",
pageurl="https://example.com",
)
except CaptchaAIError as e:
print(f"API error: {e.code}")
Dependências do projeto:
pip install pydantic requests
Erros comuns e como corrigir
A maioria dos erros abaixo aparece na primeira execução, quando os dados ainda não seguem exatamente o formato esperado pelos modelos.
Dica: rode o bloco de exemplo da seção anterior com valores propositalmente inválidos — sitekey vazio,
score_qafora do intervalo — para ver cadaValidationErrorna prática antes de apontar o cliente para dados reais.
| Problema | Causa | Correção |
|---|---|---|
ValidationError em um sitekey que parece correto |
Sitekey com menos de 20 caracteres | Confira o tamanho do sitekey; ajuste min_length se o site de teste usar chaves mais curtas |
ValidationError no pageurl |
URL sem esquema | Inclua o prefixo https:// |
| Validação da imagem em base64 falha | String curta demais ou ainda com o prefixo data: |
O validador já remove o prefixo data: sozinho; confirme que o conteúdo base64 tem mais de 100 caracteres |
CaptchaAIError: ERROR_ZERO_BALANCE |
Saldo insuficiente na conta | Recarregue os créditos no painel da CaptchaAI |
| Erro de importação do Pydantic v1 | Versão errada instalada | Use Pydantic v2: pip install 'pydantic>=2.0' |
Perguntas frequentes
A validação com Pydantic deixa a chamada mais lenta?
Não de forma perceptível. A validação roda em microssegundos; uma viagem de ida e volta até a API da CaptchaAI leva segundos. Barrar um parâmetro inválido antes do POST economiza muito mais tempo do que a validação custa.
Dá para reaproveitar esses modelos em outros tipos de CAPTCHA além dos que já estão no exemplo?
Sim. Crie uma nova subclasse de BaseModel com os campos que o tipo exige e um método to_params() que devolve o dicionário de parâmetros. Depois, adicione um método solve_* correspondente na classe CaptchaAI que instancia esse modelo e chama _solve.
Consigo usar esse cliente dentro de uma API própria feita com FastAPI?
Sim, e o encaixe é natural: os mesmos modelos Pydantic usados aqui podem servir como corpo de requisição (request body) de um endpoint FastAPI, sem duplicar a validação. A parte de submissão e polling continua igual — só muda quem chama client.solve_recaptcha_v2().
O que muda se eu migrar do Pydantic v1 para o v2?
A sintaxe de validadores muda: @validator vira @field_validator com @classmethod, e .dict() vira .model_dump(). Os exemplos deste artigo já usam a API do Pydantic v2 — se seu projeto ainda está no v1, use-os como referência para a migração.
Preciso validar a chave de API antes de cada chamada?
O construtor de CaptchaAI já faz uma checagem mínima (comprimento da chave) na inicialização do cliente, uma vez por processo. Erros de chave inválida ou saldo zerado — que só o servidor sabe informar — continuam chegando como CaptchaAIError na primeira chamada real.
Artigos relacionados
- Guia completo de Playwright com Python e CaptchaAI
- Como montar pipelines de CAPTCHA do lado do cliente com a CaptchaAI
- Validação de segurança de callback em webhooks da CaptchaAI
Próximos passos
Monte seu cliente CaptchaAI validado — pegue sua chave de API e comece a adicionar os modelos Pydantic ao seu projeto.
Guias relacionados: