Explainers

Guia de extração de URL reCAPTCHA Anchor e Bframe

Comece pela conclusão: para resolver o desafio, você não precisa extrair nada. A API da CaptchaAI recebe sitekey e pageurl e devolve o token. As URLs de anchor e bframe servem ao diagnóstico: token rejeitado, página com vários widgets, sitekey que só aparece depois do JavaScript.

Abaixo, a anatomia dos dois iframes e como ler esses parâmetros em Python, Node.js e Selenium, em ambiente próprio ou staging autorizado.


O que a API realmente exige

Duas coisas costumam se misturar nas discussões de bug:

  • Para resolver: sitekey + pageurl. O widget, o clique e o desafio ficam por conta do solucionador.
  • Para investigar: os parâmetros da URL do anchor, a fonte mais confiável do que o navegador de fato carregou.

Se a resolução falhou, a causa costuma ser sitekey errada ou domínio divergente. Ambas aparecem no anchor.


Como o reCAPTCHA v2 monta os dois iframes

O widget são dois iframes aninhados, cada um com um papel.

Target page (staging.example.com/qa-login)
    └── <iframe src="https://www.google.com/recaptcha/api2/anchor?...">
        │   ← Anchor iframe: "I'm not a robot" checkbox
        │
        └── <iframe src="https://www.google.com/recaptcha/api2/bframe?...">
                ← Bframe iframe: Image challenge grid (loads when clicked)

O iframe anchor: checkbox e análise de risco inicial

O anchor carrega com a página e traz toda a configuração do widget na query string:

https://www.google.com/recaptcha/api2/anchor?
    ar=1
    &k=6LcR_RsTAAAAAN_r0GEkGBfq3L7KmU5JbPHJtwNp  ← site key
    &co=aHR0cHM6Ly9leGFtcGxlLmNvbTo0NDM.           ← encoded origin
    &hl=en                                           ← language
    &v=jF2Zb_rr_5sv8dMHoGIn-XxY                    ← reCAPTCHA version
    &size=normal                                     ← widget size
    &cb=89fu2pf0swif                                ← callback ID

O iframe bframe: o desafio de imagens

O bframe só entra em cena quando o clique no checkbox aciona a grade de imagens:

https://www.google.com/recaptcha/api2/bframe?
    hl=en
    &v=jF2Zb_rr_5sv8dMHoGIn-XxY
    &k=6LcR_RsTAAAAAN_r0GEkGBfq3L7KmU5JbPHJtwNp

Parâmetros da URL do anchor

Parâmetro Nome Descrição
k sitekey A sitekey (chave pública do widget) do reCAPTCHA
co Origem codificada Origem em Base64 (protocolo + domínio + porta)
v Versão Hash da versão do pacote JavaScript do reCAPTCHA
hl Idioma Código do idioma usado no desafio
size Tamanho normal, compact ou invisible
cb callback Identificador único da função de callback
theme Tema light ou dark
ar Proporção Sinalizador de proporção de exibição

Dois deles fecham a maioria dos casos:

  • k — qual sitekey está em uso.
  • co — para qual domínio o widget foi configurado.

Como decodificar o parâmetro co

O co é a origem em base64, com um ponto final que é resíduo do padding:

import base64

co_value = "aHR0cHM6Ly9leGFtcGxlLmNvbTo0NDM."
# Remove trailing period (padding artifact)
decoded = base64.b64decode(co_value.rstrip(".") + "==").decode()
print(decoded)  # "https://example.com:443"

Se a origem decodificada não bate com o pageurl enviado, você achou a causa da rejeição.


Como extrair as URLs do anchor e do bframe

Extração com Python a partir do HTML

Para páginas em que o widget já vem no HTML inicial:

import requests
from bs4 import BeautifulSoup
from urllib.parse import urlparse, parse_qs
import re
import base64

def extract_recaptcha_iframes(url):
    """Extract reCAPTCHA anchor and bframe iframe URLs and parameters."""
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                      "AppleWebKit/537.36 (KHTML, like Gecko) "
                      "Chrome/120.0.0.0 Safari/537.36",
    }

    response = requests.get(url, headers=headers, timeout=15)
    soup = BeautifulSoup(response.text, "html.parser")

    result = {
        "anchor_url": None,
        "bframe_url": None,
        "site_key": None,
        "origin": None,
        "version": None,
        "language": None,
    }

    # Find anchor iframe
    anchor_iframe = soup.find("iframe", src=re.compile(r"recaptcha.*anchor"))
    if anchor_iframe:
        anchor_url = anchor_iframe.get("src", "")
        result["anchor_url"] = anchor_url

        # Parse parameters
        parsed = urlparse(anchor_url)
        params = parse_qs(parsed.query)

        result["site_key"] = params.get("k", [None])[0]
        result["version"] = params.get("v", [None])[0]
        result["language"] = params.get("hl", [None])[0]

        # Decode origin
        co = params.get("co", [None])[0]
        if co:
            try:
                padded = co.rstrip(".") + "=="
                result["origin"] = base64.b64decode(padded).decode()
            except Exception:
                result["origin"] = co

    # Find bframe iframe (may not be in source — loaded dynamically)
    bframe_iframe = soup.find("iframe", src=re.compile(r"recaptcha.*bframe"))
    if bframe_iframe:
        result["bframe_url"] = bframe_iframe.get("src", "")

    # Construct bframe URL from anchor parameters if not found
    if not result["bframe_url"] and result["site_key"] and result["version"]:
        result["bframe_url"] = (
            f"https://www.google.com/recaptcha/api2/bframe?"
            f"hl={result['language'] or 'en'}"
            f"&v={result['version']}"
            f"&k={result['site_key']}"
        )

    return result

iframes = extract_recaptcha_iframes("https://staging.example.com/qa-login")
print(f"Site key: {iframes['site_key']}")
print(f"Origin: {iframes['origin']}")
print(f"Anchor URL: {iframes['anchor_url']}")

Extração com Node.js e cheerio

Mesma lógica para pipelines de teste em JavaScript:

const axios = require("axios");
const cheerio = require("cheerio");
const { URL } = require("url");

async function extractRecaptchaIframes(pageUrl) {
    const { data: html } = await axios.get(pageUrl, {
        headers: {
            "User-Agent":
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +
                "AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
        },
        timeout: 15000,
    });

    const $ = cheerio.load(html);
    const result = {
        anchorUrl: null,
        bframeUrl: null,
        siteKey: null,
        origin: null,
        version: null,
    };

    // Find anchor iframe
    const anchorIframe = $("iframe[src*='recaptcha'][src*='anchor']");
    if (anchorIframe.length) {
        const src = anchorIframe.attr("src");
        result.anchorUrl = src;

        const url = new URL(src);
        result.siteKey = url.searchParams.get("k");
        result.version = url.searchParams.get("v");

        // Decode origin
        const co = url.searchParams.get("co");
        if (co) {
            try {
                result.origin = Buffer.from(
                    co.replace(/\.$/, ""), "base64"
                ).toString();
            } catch {}
        }
    }

    // Construct bframe URL
    if (result.siteKey && result.version) {
        result.bframeUrl =
            `https://www.google.com/recaptcha/api2/bframe?` +
            `hl=en&v=${result.version}&k=${result.siteKey}`;
    }

    return result;
}

extractRecaptchaIframes("https://staging.example.com/qa-login").then(console.log);

Extração com Selenium em páginas dinâmicas

Quando o widget só aparece depois que o JavaScript executa, o HTML estático não basta:

from selenium import webdriver
from selenium.webdriver.common.by import By
import time

def extract_iframes_selenium(url):
    """Extract reCAPTCHA iframe URLs from a dynamically loaded page."""
    driver = webdriver.Chrome()
    driver.get(url)
    time.sleep(3)  # Wait for reCAPTCHA to load

    result = {"anchor_url": None, "bframe_url": None}

    # Find all iframes
    iframes = driver.find_elements(By.TAG_NAME, "iframe")

    for iframe in iframes:
        src = iframe.get_attribute("src") or ""
        if "recaptcha" in src and "anchor" in src:
            result["anchor_url"] = src
        elif "recaptcha" in src and "bframe" in src:
            result["bframe_url"] = src

    driver.quit()
    return result

Quatro situações em que a extração compensa

1. Confirmar qual sitekey está em uso

Com vários formulários na página, ou sitekey injetada em runtime, o HTML é pouco confiável. O anchor não:

# Extract sitekey from anchor URL when it's not in the page HTML
iframes = extract_recaptcha_iframes(url)
sitekey = iframes["site_key"]  # Reliably present in the iframe URL

2. Identificar a variante do reCAPTCHA

O caminho da URL separa o v2 comum do Enterprise:

# The anchor URL reveals the exact reCAPTCHA version
if "/api2/anchor" in anchor_url:
    recaptcha_type = "v2"
elif "/enterprise/anchor" in anchor_url:
    recaptcha_type = "enterprise"

3. Alinhar a origem em implementações restritas por domínio

# Decode the origin from the co parameter
origin = decode_co_parameter(iframes["co"])
# Use this origin as the pageurl for the solver

4. Investigar tokens rejeitados

def debug_solve_params(anchor_url, solver_pageurl, solver_sitekey):
    """Compare anchor params with solver request to find mismatches."""
    parsed = urlparse(anchor_url)
    params = parse_qs(parsed.query)

    issues = []

    # Check sitekey
    anchor_key = params.get("k", [None])[0]
    if anchor_key != solver_sitekey:
        issues.append(f"Sitekey mismatch: anchor={anchor_key}, solver={solver_sitekey}")

    # Check origin
    co = params.get("co", [None])[0]
    if co:
        origin = base64.b64decode(co.rstrip(".") + "==").decode()
        solver_parsed = urlparse(solver_pageurl)
        solver_origin = f"{solver_parsed.scheme}://{solver_parsed.netloc}"
        if origin != solver_origin:
            issues.append(f"Origin mismatch: anchor={origin}, solver={solver_origin}")

    return issues if issues else ["No mismatches found"]

Um cenário de QA comum

Um time em São Paulo mantém testes de integração de um checkout interno, com workers em sa-east-1. Em staging o reCAPTCHA v2 é resolvido normalmente; em pré-produção o token passa a ser recusado.

A extração do anchor nos dois ambientes fecha o caso:

  • O co decodificado aponta para o host de staging nos dois: a sitekey foi copiada junto com a configuração e nunca trocada.
  • Não era latência nem o solucionador — era sitekey herdada.

Mantenha o diagnóstico em ambientes que você controla. Se os logs guardarem dados reais, considere as obrigações da LGPD (RGPD, em Portugal) na retenção: registrar sitekey, origem e versão é inofensivo; payloads de formulário, não.


O caminho padrão: sitekey + pageurl

Para o fluxo normal, ignore os iframes e envie a tarefa direto:

import requests
import time

API_KEY = "YOUR_API_KEY"

# All you need: sitekey + pageurl
submit = requests.post("https://ocr.captchaai.com/in.php", data={
    "key": API_KEY,
    "method": "userrecaptcha",
    "googlekey": "6LcR_RsTAAAAAN_r0GEkGBfq3L7KmU5JbPHJtwNp",
    "pageurl": "https://staging.example.com/qa-login",
    "json": 1,
})

task_id = submit.json()["request"]

for _ in range(60):
    time.sleep(5)
    result = requests.get("https://ocr.captchaai.com/res.php", params={
        "key": API_KEY,
        "action": "get",
        "id": task_id,
        "json": 1,
    }).json()

    if result.get("status") == 1:
        token = result["request"]
        print(f"Token: {token[:50]}...")
        break

Os planos da CaptchaAI são cobrados por thread — cada thread é um CAPTCHA em andamento:

  • BASIC (US$ 15/mês, 5 threads): cobre uma suíte de integração diária.
  • ADVANCE (US$ 90/mês, 50 threads): para monitoramento contínuo.

Perguntas frequentes

A mesma extração funciona no reCAPTCHA Enterprise?

Sim. O Enterprise carrega o anchor em /recaptcha/enterprise/anchor, e não em /recaptcha/api2/anchor; os parâmetros k, co e v ficam no mesmo lugar.

Por que o bframe não aparece no HTML inicial?

Porque ele só é criado quando o clique no checkbox resulta em desafio de imagens. Capturá-lo exige um navegador controlado (Selenium ou Puppeteer) e interação real com o widget.

Consigo pegar a sitekey sem abrir um navegador?

Na maioria das páginas, sim: uma requisição HTTP com requests ou axios já traz o iframe do anchor, e a sitekey está no k. O Selenium só é necessário quando o widget é renderizado no cliente.

O parâmetro hl muda o resultado da resolução?

Não. O hl define apenas o idioma exibido ao visitante; não altera o token nem precisa ser replicado na tarefa.


Resumo

O reCAPTCHA v2 vive em dois iframes: anchor (checkbox e configuração) e bframe (desafio de imagens). A URL do anchor carrega sitekey, origem em base64 e hash de versão — os valores que resolvem quase toda investigação de token rejeitado. Para o uso normal com a CaptchaAI, sitekey e pageurl bastam.

Artigos relacionados

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