# 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.

- **Duas maneiras de entrar:** um proxy direto em `unlock.quantumproxies.io:9000` para código que já fala protocolo de proxy, ou `POST /api/v1/scraper/unlock` com sua chave de API para todo o resto.
- **O que você recebe de volta:** o status, cabeçalhos e corpo da origem, além dos cabeçalhos `x-qp-*` dizendo quantas tentativas foram necessárias, se um navegador foi executado, e que tipo de barreira o parou, se houve uma.
- **Quando você não precisa dele:** Wikipedia, GitHub, docs.python.org, Hacker News e books.toscrape.com responderam a solicitações simples em nosso teste. Tente simples primeiro e escale apenas em um bloqueio detectado.

## O que você precisa antes de começar

- Uma conta QuantumProxies.io com largura de banda do Web Unlocker. O desbloqueador cobra de um saldo pré-pago de GB, separado para o nível residencial e o nível móvel (saídas de operadora 4G/5G).
- Para o proxy direto: um sub-usuário de proxy criado na [página do Web Unlocker do painel](https://app.quantumproxies.io/unlock). Parece `unlock-XXXXXXXX` com sua própria senha. Não é sua chave de API.
- Para o endpoint REST: uma chave de API do painel, enviada como `Authorization: Bearer`.
- Python 3.10 ou mais recente com `requests`, e para a parte Node.js Node 18 ou mais recente com o pacote `undici`.

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.

```python
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](https://quantumproxies.io/blog/tls-ja3-fingerprinting-scraping). Um `403` significa que o servidor entendeu a solicitação e a recusou ([RFC 9110, seção 15.5.4](https://www.rfc-editor.org/rfc/rfc9110.html#name-403-forbidden)). 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.

```python
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:

```bash
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](https://requests.readthedocs.io/en/latest/user/advanced/) 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`.

```python
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`.

![Um GET através do Web Unlocker: até três tentativas TLS com novos IPs de saída e novas impressões digitais de navegador, um navegador headless apenas se aparecer um desafio JavaScript e, por fim, a página real ou um bloqueio rotulado. O quadro lista os cabeçalhos x-qp que informam as tentativas, a renderização e a classe do bloqueio.](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1790701775/blog/generated/qp-unl-g1-diag1.png)
*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](https://quantumproxies.io/web-unlocker)

## 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çalho | O que ele lhe diz |
| --- | --- |
| x-qp-unlocker-attempts | Quantas tentativas a solicitação levou, incluindo tentativas |
| x-qp-unlocker-rendered / x-qp-unlocker-escalated | Definido como 1 quando um navegador sem cabeça buscou a página / quando a solicitação subiu do nível TLS |
| x-qp-unlocker-profile | Qual impressão digital de navegador a tentativa retornada usou |
| x-qp-clearance | O que aconteceu com o cache de liberação do navegador nesta chamada: minted, hit ou miss |
| x-qp-session / x-qp-country | A sessão de saída e o país realmente usados |
| x-qp-unlocker-blocked | Presente apenas quando a página ainda estava bloqueada após todas as tentativas |
| x-qp-block-class / x-qp-vendor | Que 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:

```python
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](https://github.com/nodejs/undici/blob/main/docs/docs/api/ProxyAgent.md) 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.

```javascript
// 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+:

```javascript
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:

| Site | Simples, servidor de datacenter | Simples, proxy residencial | Web Unlocker |
| --- | --- | --- | --- |
| docs.python.org (página urllib.request) | 200 | 200 | 200, 1,6 s |
| books.toscrape.com | 200 | 200 | 200, 1,9 s |
| news.ycombinator.com | 200 | 200 | 200, 4,1 s |
| en.wikipedia.org (Web scraping) | 200 | 200 | 200, 1,9 s |
| github.com (repositório scrapy) | 200 | 200 | 200, 2,0 s |
| bbc.com (Tecnologia) | redirecionamento 302 | redirecionamento 302 | 200, 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:

```python
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.

| Site | Simples, servidor de datacenter | Simples, proxy residencial | Web 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 captcha | 403, 'Ativar JavaScript e cookies para continuar' | 200, 2 tentativas + navegador, 17,2 s |
| Walmart (busca) | redirecionamento 307, sem página de resultados | redirecionamento 307, sem página de resultados | 200, 1 tentativa, 4,8 s |
| Best Buy (categoria de laptops) | Conexão falhou, sem resposta | Conexão falhou, sem resposta | 200, 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](https://quantumproxies.io/blog/scrape-indeed-job-postings) cobre o que fazer com a página uma vez que você a tenha.

![A verificação de bloqueio do tutorial, que tenta primeiro a solicitação simples: uma função looks_blocked sinaliza uma resposta pelo código de status, 403, 429 ou 503, pelo cabeçalho cf-mitigated: challenge ou pelo título de uma página de bloqueio, e só então a URL é enviada pelo Web Unlocker. Não se varre o corpo inteiro porque isso sinalizou a Wikipedia.](https://res.cloudinary.com/dmqliab0t/image/upload/f_auto,q_auto,w_1200/v1790750868/blog/generated/qp-unl-g1-diag2.png)
*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](https://quantumproxies.io/blog/curl-403-but-browser-works) aprofunda o diagnóstico. Para uma visão site por site de qual camada você realmente precisa, veja [web unlocker vs proxy residencial, testado](https://quantumproxies.io/blog/web-unlocker-vs-residential-proxy).

## 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](https://quantumproxies.io/web-unlocker/residential)
