Tutorial da API Web Unlocker: Python e Node.js, Bloqueado vs Desbloqueado

Código funcional em Python e Node.js para uma API de desbloqueio web: a mesma solicitação contra um site que bloqueia clientes comuns e um que não, como ler o que o desbloqueador relata, e um auxiliar que só paga pelo desbloqueador quando um bloqueio é detectado.

Uma API de desbloqueio web reproduz uma solicitação HTTP de um IP residencial sob uma impressão digital de TLS de navegador, tentando novamente com um IP novo e mudando para um navegador real quando o site responde com um desafio. Este tutorial da API de desbloqueio web mostra isso em Python e Node.js, em um site que bloqueia clientes comuns e um que não.

Cada resultado citado abaixo foi medido em 29 de setembro de 2026: a mesma URL buscada com curl simples de um servidor de datacenter em Frankfurt, com curl simples através de um proxy residencial com saída nos EUA, e através do Web Unlocker da QuantumProxies.io com saída nos EUA. Onde um site não precisou do desbloqueador, dizemos isso, porque a solicitação mais barata é a que funciona sem ele.

O que você precisa antes de começar

Mantenha ambos os segredos em variáveis de ambiente. Os exemplos leem QP_UNLOCK_USER, QP_UNLOCK_PASSWORD e QP_API_KEY. Se a senha do proxy contiver caracteres como @ ou :, codifique a URL com urllib.parse.quote antes de colocá-la em uma URL de proxy.

Passo 1: veja o bloqueio com Python requests simples

Comece com a falha, para saber o que está corrigindo. O alvo é uma busca de emprego no Indeed. Do nosso servidor de datacenter em Frankfurt, respondeu 403 com uma página intitulada 'Verificação de Segurança - Indeed.com'. Através de um proxy residencial simples, ainda respondeu 403, desta vez com um corpo pedindo ao cliente para 'Ativar JavaScript e cookies para continuar'. Um IP doméstico limpo não foi suficiente.

import re
import requests

URL = "https://www.indeed.com/jobs?q=data+engineer&l=Austin%2C+TX"
HEADERS = {
    "User-Agent": (
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
        "(KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36"
    ),
    "Accept-Language": "en-US,en;q=0.9",
}


def title_of(html: str) -> str:
    m = re.search(r"<title[^>]*>(.*?)</title>", html, re.I | re.S)
    return m.group(1).strip() if m else ""


r = requests.get(URL, headers=HEADERS, timeout=30)
print(r.status_code, "|", title_of(r.text))
# Our datacenter run on 29 Sep 2026 saw: 403 | Security Check - Indeed.com

Nossa medição usou curl com um User-Agent do Chrome, não requests, e não há razão para esperar que requests se saia melhor. Uma string User-Agent é um cabeçalho. A camada anti-bot também lê o handshake TLS e as configurações HTTP/2, e ainda dizem Python, qualquer que seja o User-Agent que você defina. É por isso que trocar cabeçalhos raramente faz você passar pela detecção de bot em Python requests; o lado do handshake é coberto em nosso guia sobre impressão digital TLS e por que IPs limpos ainda são bloqueados. Um 403 significa que o servidor entendeu a solicitação e a recusou (RFC 9110, seção 15.5.4). Não significa que sua URL ou seus parâmetros estão errados.

Passo 2: a mesma chamada através do proxy direto

O proxy direto é a menor mudança no código existente: passe-o como um proxy e nada mais se move. O direcionamento vai no nome de usuário. -country-us escolhe o país de saída, -tier-mobile muda para saídas móveis, e -profile-firefox (ou chrome, safari, safariios, edge, brave, mobile) fixa uma impressão digital TLS em vez de deixar as tentativas girarem.

import os
import requests
import urllib3

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

user = os.environ["QP_UNLOCK_USER"] + "-country-us"   # e.g. unlock-XXXXXXXX-country-us
password = os.environ["QP_UNLOCK_PASSWORD"]
PROXY = f"http://{user}:{password}@unlock.quantumproxies.io:9000"

r = requests.get(
    URL,
    headers=HEADERS,
    proxies={"http": PROXY, "https": PROXY},
    verify=False,   # or verify="qp-unlocker-ca.pem", see below
    timeout=120,
)
print(r.status_code, "|", title_of(r.text))
print("attempts:", r.headers.get("x-qp-unlocker-attempts"),
      "rendered:", r.headers.get("x-qp-unlocker-rendered"))

Em 29 de setembro, isso retornou 200 com a página de resultados reais, intitulada 'Agora Contratando: 2.000 Vagas de Engenheiro de Dados em Austin, TX | Indeed', na primeira tentativa de TLS, em 2,6 segundos, sem iniciar um navegador.

Por que verify=False? Para reescrever uma solicitação HTTPS, o desbloqueador precisa terminar o TLS do cliente e apresentar seu próprio certificado para o host de destino. Você tem duas opções limpas. Desative a verificação apenas para esta sessão, como acima, que é o que curl -k faz. Ou baixe o CA do desbloqueador uma vez e confie explicitamente, para que a verificação permaneça ativada:

curl -H "Authorization: Bearer $QP_API_KEY" \
  -o qp-unlocker-ca.pem https://app.quantumproxies.io/api/v1/scraper/unlock/ca

Em seguida, passe verify="qp-unlocker-ca.pem" em vez de False. Os documentos de uso avançado do requests cobrem tanto o argumento proxies quanto pacotes CA personalizados. Se você não quiser nenhum dos dois, o endpoint REST abaixo não precisa de certificado algum.

Passo 3: o endpoint REST, sem protocolo de proxy

O endpoint REST executa o mesmo mecanismo por trás de uma chamada HTTPS normal. Você descreve a solicitação que teria feito (url, método, cabeçalhos, corpo) e obtém a resposta da origem de volta como JSON. O único campo obrigatório é url.

import os
import requests

API_KEY = os.environ["QP_API_KEY"]

resp = requests.post(
    "https://app.quantumproxies.io/api/v1/scraper/unlock",
    headers={"Authorization": f"Bearer {API_KEY}"},
    json={
        "url": URL,
        "country": "us",
        # "tier": "mobile",        # mobile exits, billed on the mobile balance
        # "failOnBlock": True,     # HTTP 502 instead of 200 when every tier was blocked
    },
    timeout=130,
)
resp.raise_for_status()           # 402 = no unlocker GB left, 400 = bad input
data = resp.json()["payload"]

print(data["status"], "blocked:", data["blocked"])
print("attempts:", data["attempts"], "rendered:", data["rendered"],
      "escalated:", data["escalated"], "clearance:", data.get("clearance"))
if data["blocked"]:
    print("block class:", data.get("blockClass"), "vendor:", data.get("vendor"))
html = data.get("body") or ""     # text form; bodyBase64 is always present too

O envelope de resposta é { type, message, payload }. Dentro de payload você obtém status (o código de status da origem), headers, body e bodyBase64, finalUrl, a impressão digital profile, attempts, rendered, escalated, blocked e, quando algo parou a solicitação, blockClass e vendor. Um objeto usage relata os bytes debitados pela chamada.

Um comportamento para projetar: por padrão, uma solicitação que permaneceu bloqueada após todas as tentativas ainda volta da API como HTTP 200, com blocked: true e a página de bloqueio em body para inspeção. Ela é rotulada, não disfarçada. Se seu pipeline trata qualquer 200 como sucesso, defina failOnBlock: true e você receberá um 502 com os mesmos diagnósticos. Outros campos que vale a pena conhecer: method e body para POSTs, render: "html" para iniciar em um navegador, waitForSelector e returnCookies.

Fluxo de uma solicitação Web Unlocker: até três tentativas de TLS em diferentes IPs de saída e impressões digitais de navegador, depois um navegador sem cabeça se aparecer um desafio JavaScript, então a página ou um bloqueio rotulado
Um GET através do desbloqueador: tentativas de TLS primeiro, um navegador real apenas quando um desafio JavaScript aparece, e um resultado rotulado de qualquer maneira.

Veja como o Web Unlocker lida com solicitações bloqueadas

Passo 4: leia os cabeçalhos x-qp e lide com um bloqueio relatado

Através do proxy direto, o desbloqueador relata na própria resposta. Estes são os cabeçalhos que vale a pena registrar em cada chamada:

CabeçalhoO que ele lhe diz
x-qp-unlocker-attemptsQuantas tentativas a solicitação levou, incluindo tentativas
x-qp-unlocker-rendered / x-qp-unlocker-escalatedDefinido como 1 quando um navegador sem cabeça buscou a página / quando a solicitação subiu do nível TLS
x-qp-unlocker-profileQual impressão digital de navegador a tentativa retornada usou
x-qp-clearanceO que aconteceu com o cache de liberação do navegador nesta chamada: minted, hit ou miss
x-qp-session / x-qp-countryA sessão de saída e o país realmente usados
x-qp-unlocker-blockedPresente apenas quando a página ainda estava bloqueada após todas as tentativas
x-qp-block-class / x-qp-vendorQue tipo de barreira foi, e qual fornecedor anti-bot, quando reconhecido

Uma página que ainda está bloqueada nunca chega como um 200 silencioso. Se a origem recusou com seu próprio erro (403, 429, 503), esse status é retransmitido sem alterações com x-qp-unlocker-blocked definido. Se a origem serviu uma página de desafio com um 2xx, o proxy a transforma em um 502 e coloca o código da origem em x-qp-origin-status. Então, uma verificação cobre ambos:

class Blocked(Exception):
    pass


s = requests.Session()
s.proxies = {"http": PROXY, "https": PROXY}
s.verify = False  # or "qp-unlocker-ca.pem"

NEXT_STEP = {
    "js_challenge":  "a browser could not clear it; retry later in a pinned session",
    "captcha":       "interactive captcha; decide in your own code",
    "access_denied": "the site denied access; retry with a fresh session or test tier-mobile",
    "rate_limited":  "the site is rate limiting; slow down and retry later",
    "fingerprint":   "the client fingerprint was refused; retry with another profile",
    "geo":           "the content depends on location; set the country explicitly",
    "timeout":       "the target was too slow; raise x-qp-timeout or retry",
}


def unlock_get(url: str, session_id: str | None = None) -> requests.Response:
    headers = {"x-qp-session": session_id} if session_id else {}
    r = s.get(url, headers=headers, timeout=120)
    if "x-qp-unlocker-blocked" in r.headers:
        cls = r.headers.get("x-qp-block-class", "unknown")
        raise Blocked(
            f"{url}: HTTP {r.status_code}, class={cls}, "
            f"vendor={r.headers.get('x-qp-vendor')}, next: {NEXT_STEP.get(cls, 'inspect the body')}"
        )
    return r

Passo 5: a versão em Node.js (API web unlocker em JavaScript)

No Node, fetch aceita um dispatcher. Use ProxyAgent do undici para o proxy direto, e importe fetch do mesmo pacote para que as duas versões correspondam. A documentação do ProxyAgent lista as opções usadas aqui: token para o valor de Proxy-Authorization e requestTls para as configurações de TLS em direção ao alvo.

// unlock.mjs  (npm i undici)
import { readFileSync } from "node:fs";
import { fetch, ProxyAgent } from "undici";

const user = process.env.QP_UNLOCK_USER + "-country-us";
const pass = process.env.QP_UNLOCK_PASSWORD;

const dispatcher = new ProxyAgent({
  uri: "http://unlock.quantumproxies.io:9000",
  token: "Basic " + Buffer.from(user + ":" + pass).toString("base64"),
  requestTls: { rejectUnauthorized: false },
  // verified instead: requestTls: { ca: readFileSync("qp-unlocker-ca.pem") },
});

const url = "https://www.indeed.com/jobs?q=data+engineer&l=Austin%2C+TX";
const res = await fetch(url, { dispatcher, headers: { "x-qp-session": "indeed-001" } });

if (res.headers.get("x-qp-unlocker-blocked")) {
  console.error("blocked:", res.status, res.headers.get("x-qp-block-class"), res.headers.get("x-qp-vendor"));
} else {
  const html = await res.text();
  console.log(res.status, res.headers.get("x-qp-unlocker-attempts"), html.length);
}

O endpoint REST não precisa de nada além do fetch embutido no Node 18+:

const resp = await fetch("https://app.quantumproxies.io/api/v1/scraper/unlock", {
  method: "POST",
  headers: {
    Authorization: "Bearer " + process.env.QP_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ url: "https://www.indeed.com/jobs?q=data+engineer&l=Austin%2C+TX", country: "us" }),
});
const { payload } = await resp.json();
console.log(payload.status, payload.blocked, payload.blockClass ?? "-", payload.attempts);

No axios, o padrão usual para alvos HTTPS por trás de um proxy é passar um agente de https-proxy-agent como httpsAgent e definir proxy: false, ou chamar o endpoint REST acima e pular o protocolo de proxy completamente.

Passo 6: quando você não precisa do desbloqueador

O desbloqueador é cobrado por GB em cada byte que move, incluindo tentativas e carregamentos de página do navegador. Enviá-lo páginas que uma solicitação simples busca bem queima saldo por nada. Em nossa execução, estes voltaram bem sem qualquer ajuda:

SiteSimples, servidor de datacenterSimples, proxy residencialWeb Unlocker
docs.python.org (página urllib.request)200200200, 1,6 s
books.toscrape.com200200200, 1,9 s
news.ycombinator.com200200200, 4,1 s
en.wikipedia.org (Web scraping)200200200, 1,9 s
github.com (repositório scrapy)200200200, 2,0 s
bbc.com (Tecnologia)redirecionamento 302redirecionamento 302200, 1,4 s

A linha da BBC não é um bloqueio. Nosso cliente de teste não seguiu redirecionamentos, e o 302 é o site enviando você para outra URL. requests segue redirecionamentos por padrão e chega na página.

Então o padrão certo é simples primeiro, desbloqueador em um bloqueio detectado. A detecção precisa de cuidado. A varredura de marcadores em nosso próprio teste marcou as páginas da Wikipedia e do GitHub como bloqueadas porque seus corpos contêm a palavra 'captcha', mesmo que ambos tenham retornado a página real com um 200. Verifique o código de status, o cabeçalho cf-mitigated do Cloudflare e o título da página, não o corpo inteiro:

from urllib.parse import urlsplit

BLOCK_STATUSES = {403, 429, 503}
BLOCK_TITLES = (
    "just a moment",                        # Cloudflare challenge
    "attention required",                   # Cloudflare block page
    "access denied",                        # Akamai and others
    "security check",
    "access to this page has been denied",  # PerimeterX
)
needs_unlocker: set[str] = set()            # domains that blocked plain requests


def looks_blocked(r: requests.Response) -> bool:
    if r.status_code in BLOCK_STATUSES:
        return True
    if r.headers.get("cf-mitigated") == "challenge":
        return True
    return any(t in title_of(r.text).lower() for t in BLOCK_TITLES)


def fetch(url: str, session_id: str | None = None) -> tuple[requests.Response, str]:
    host = urlsplit(url).hostname or ""
    if host not in needs_unlocker:
        try:
            r = requests.get(url, headers=HEADERS, timeout=20)
            if not looks_blocked(r):
                return r, "plain"
        except requests.RequestException:
            pass  # a reset or a timeout counts as a block too
        needs_unlocker.add(host)
    return unlock_get(url, session_id), "unlocker"   # from step 4; raises Blocked


for u in ("https://docs.python.org/3/library/urllib.request.html", URL):
    resp, route = fetch(u)
    print(route, resp.status_code, title_of(resp.text)[:60])

O ramo except importa. A categoria de laptops da Best Buy nunca deu aos nossos clientes simples uma resposta HTTP: a conexão falhou tanto do servidor de datacenter quanto do proxy residencial. Através do desbloqueador, a mesma URL retornou 200 na primeira tentativa em 6,3 segundos. Um scraper que só verifica códigos de status nunca escalaria esse.

Resultados medidos: quatro sites que precisaram do desbloqueador

Medido em 29 de setembro de 2026. Solicitações simples foram curl com um User-Agent do Chrome, uma vez de um servidor de datacenter em Frankfurt e uma vez através de um proxy residencial com saída nos EUA. Solicitações do Web Unlocker usaram o nível residencial com saída nos EUA. Os tempos são de relógio de parede para uma única solicitação.

SiteSimples, servidor de datacenterSimples, proxy residencialWeb Unlocker
Indeed (busca de emprego)403, página 'Verificação de Segurança'403, 'Ativar JavaScript e cookies para continuar'200, 1 tentativa, 2,6 s
Glassdoor (avaliações de empresas)403, página 'Segurança' com captcha403, 'Ativar JavaScript e cookies para continuar'200, 2 tentativas + navegador, 17,2 s
Walmart (busca)redirecionamento 307, sem página de resultadosredirecionamento 307, sem página de resultados200, 1 tentativa, 4,8 s
Best Buy (categoria de laptops)Conexão falhou, sem respostaConexão falhou, sem resposta200, 1 tentativa, 6,3 s

Em resumo: Indeed, Walmart e Best Buy passaram na primeira tentativa de TLS sem navegador. Glassdoor precisou do caminho completo, duas tentativas de TLS e depois um navegador sem cabeça para responder ao desafio JavaScript, por isso levou 17,2 segundos. Para o Walmart, não registramos para onde o 307 apontou. O que podemos dizer é que nenhum cliente simples recebeu a página de resultados e o desbloqueador sim.

Solicitações repetidas para um site protegido por desafios ficam mais rápidas quando você fixa uma sessão. Envie o mesmo valor de cabeçalho x-qp-session, ou mantenha o mesmo -session-<id> no nome de usuário do proxy, e o cookie de liberação ganho pelo navegador é reutilizado. Em um teste do Glassdoor em 29 de setembro de 2026, com a sessão fixada no nome de usuário e duas conexões separadas, a primeira solicitação levou 14,0 segundos através do navegador e a segunda 1,4 segundos, com x-qp-clearance: hit. Nosso guia para scraping de postagens de emprego no Indeed cobre o que fazer com a página uma vez que você a tenha.

Diagrama de decisão para um scraper: tente uma solicitação simples primeiro, e envie a solicitação através do web unlocker apenas quando o status, o cabeçalho cf-mitigated ou o título da página mostrar um bloqueio
Simples primeiro, desbloqueador em um bloqueio detectado. O desbloqueador cobra por cada byte que move, então a solicitação mais barata é a que funciona sem ele.

Se você chegou aqui de um 403 que só acontece fora do navegador, nosso explicador sobre por que curl recebe um 403 quando o navegador funciona aprofunda o diagnóstico. Para uma visão site por site de qual camada você realmente precisa, veja web unlocker vs proxy residencial, testado.

Perguntas frequentes

Existe uma API de desbloqueio web gratuita?

As peças de código aberto são gratuitas: requests, curl_cffi para TLS semelhante ao de navegador, e Playwright para um navegador real. Executá-las bem é o trabalho, porque você também precisa de IPs de saída limpos e um navegador para desafios JavaScript. No QuantumProxies.io, criar uma conta é gratuito, e o tráfego do desbloqueador é cobrado por GB de um saldo pré-pago que você recarrega no painel.

Você pode fazer scraping do Glassdoor ou Zillow com Python?

Em nosso teste de 29 de setembro de 2026, solicitações simples para uma página de avaliações do Glassdoor receberam um 403 tanto de um servidor de datacenter quanto de um proxy residencial, e o desbloqueador retornou a página após uma escalada de navegador. Zillow bloqueou o servidor de datacenter, mas serviu a página através de um proxy residencial simples. Verifique os termos de cada site antes de coletar qualquer coisa.

O scraping web pode ser detectado?

Sim. Os sites avaliam o tipo de rede e histórico do IP, o handshake TLS, as configurações HTTP/2, a ordem dos cabeçalhos e, em páginas de desafio, o que o JavaScript relata sobre o navegador. Uma solicitação Python simples falha em vários desses de uma vez. É por isso que mudar apenas o User-Agent raramente ajuda, e por que o desbloqueador muda o IP, a impressão digital e, quando necessário, todo o cliente.

Preciso instalar um certificado para usar o proxy direto?

Apenas para HTTPS através do proxy direto clássico, porque o desbloqueador termina o TLS para reescrever a solicitação. Ou pule a verificação para essa sessão (verify=False em Python, -k em curl) ou baixe o CA de /api/v1/scraper/unlock/ca com sua chave de API e confie nele. O endpoint REST não precisa de certificado algum.

Como mantenho o mesmo IP em várias solicitações?

Envie o mesmo valor de cabeçalho x-qp-session em cada solicitação através do proxy direto, ou coloque o mesmo -session-<id> no nome de usuário do proxy. As chamadas que compartilham essa sessão saem da mesma saída, e um cookie de liberação ganho pelo navegador é reutilizado. No endpoint REST, passe um sessionId com rotação definida como sticky. Uma sessão sticky pode durar de 3 a 1.440 minutos.

O padrão inteiro cabe em uma frase: busque de forma simples, detecte um bloqueio por status, cabeçalho e título, envie apenas essas URLs através do desbloqueador, e registre os cabeçalhos x-qp para saber quanto cada página lhe custou. Comece com o nível residencial, e mantenha o nível Móvel para alvos que também recusam faixas residenciais.

Experimente o Web Unlocker em saídas residenciais