Web Unlocker API-handleiding: Python en Node.js, Geblokkeerd vs. Ongeblokkeerd

Werkende Python- en Node.js-code voor een web unlocker API: dezelfde aanvraag tegen een site die gewone clients blokkeert en een die dat niet doet, hoe te lezen wat de unlocker rapporteert, en een helper die alleen betaalt voor de unlocker wanneer een blokkade wordt gedetecteerd.

Een web unlocker API herhaalt een HTTP-aanvraag vanaf een residentieel IP onder een browser TLS-vingerafdruk, probeert opnieuw met een nieuw IP en schakelt over naar een echte browser wanneer de site reageert met een uitdaging. Deze web unlocker API-handleiding laat het zien in Python en Node.js, op een site die gewone clients blokkeert en een die dat niet doet.

Elk resultaat hieronder geciteerd is gemeten op 29 september 2026: dezelfde URL opgehaald met gewone curl vanaf een datacenter server in Frankfurt, met gewone curl via een residentiële proxy met een Amerikaanse exit, en via de QuantumProxies.io Web Unlocker met een Amerikaanse exit. Waar een site de unlocker niet nodig had, zeggen we dat, omdat de goedkoopste aanvraag degene is die zonder werkt.

Wat je nodig hebt voordat je begint

Bewaar beide geheimen in omgevingsvariabelen. De voorbeelden lezen QP_UNLOCK_USER, QP_UNLOCK_PASSWORD en QP_API_KEY. Als het proxy-wachtwoord tekens zoals @ of : bevat, URL-codeer het met urllib.parse.quote voordat je het in een proxy-URL plaatst.

Stap 1: zie de blokkade met gewone Python requests

Begin met de mislukking, zodat je weet wat je aan het oplossen bent. Het doel is een Indeed-vacaturezoekopdracht. Vanaf onze Frankfurt datacenter server antwoordde het 403 met een pagina getiteld 'Security Check - Indeed.com'. Via een gewone residentiële proxy antwoordde het nog steeds 403, dit keer met een body die de client vroeg om 'JavaScript en cookies in te schakelen om door te gaan'. Een schoon thuis-IP was niet genoeg.

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

Onze meting gebruikte curl met een Chrome User-Agent, niet requests, en er is geen reden om te verwachten dat requests het beter doet. Een User-Agent string is één header. De anti-botlaag leest ook de TLS-handshake en de HTTP/2-instellingen, en die zeggen nog steeds Python, ongeacht welke User-Agent je instelt. Daarom helpt het zelden om alleen headers te wisselen om botdetectie in Python requests te omzeilen; de handshakeskant wordt behandeld in onze gids over TLS-vingerafdrukken en waarom schone IP's nog steeds worden geblokkeerd. Een 403 betekent dat de server de aanvraag begreep en weigerde (RFC 9110, sectie 15.5.4). Het betekent niet dat je URL of je parameters verkeerd zijn.

Stap 2: dezelfde oproep via de forward proxy

De forward proxy is de kleinste verandering aan bestaande code: geef het door als een proxy en niets anders beweegt. Doelgerichtheid rijdt in de gebruikersnaam. -country-us kiest het exitland, -tier-mobile schakelt over naar mobiele exits, en -profile-firefox (of chrome, safari, safariios, edge, brave, mobile) vergrendelt één TLS-vingerafdruk in plaats van het laten roteren bij herhalingen.

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"))

Op 29 september gaf dit 200 met de echte resultatenpagina, getiteld 'Now Hiring: 2,000 Data Engineer Jobs in Austin, TX | Indeed', bij de eerste TLS-poging, in 2,6 seconden, zonder een browser te starten.

Waarom verify=False? Om een HTTPS-aanvraag te herschrijven, moet de unlocker de TLS van je client beëindigen en zijn eigen certificaat voor de doelhost presenteren. Je hebt twee schone opties. Schakel verificatie uit voor deze sessie alleen, zoals hierboven, wat is wat curl -k doet. Of download de unlocker CA eenmaal en vertrouw het expliciet, zodat verificatie aan blijft:

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

Geef dan verify="qp-unlocker-ca.pem" door in plaats van False. De requests geavanceerde gebruiksdocumentatie behandelt zowel het proxies argument als aangepaste CA-bundels. Als je geen van beide wilt, heeft het REST-eindpunt hieronder helemaal geen certificaat nodig.

Stap 3: het REST-eindpunt, geen proxy-protocol

Het REST-eindpunt draait dezelfde engine achter een normale HTTPS-aanroep. Je beschrijft de aanvraag die je zou hebben gedaan (url, methode, headers, body) en krijgt het antwoord van de oorsprong terug als JSON. Het enige vereiste veld is 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

De responsenvelop is { type, message, payload }. Binnen payload krijg je status (de statuscode van de oorsprong), headers, body en bodyBase64, finalUrl, de vingerafdruk profile, attempts, rendered, escalated, blocked en, wanneer iets de aanvraag stopte, blockClass en vendor. Een usage object rapporteert de bytes die in rekening zijn gebracht voor de oproep.

Eén gedrag om omheen te ontwerpen: standaard komt een aanvraag die na elke poging geblokkeerd bleef nog steeds terug van de API als HTTP 200, met blocked: true en de blokpagina in body voor inspectie. Het is gelabeld, niet vermomd. Als je pijplijn elke 200 als succes behandelt, stel failOnBlock: true in en je krijgt een 502 met dezelfde diagnostiek in plaats daarvan. Andere velden die de moeite waard zijn om te weten: method en body voor POSTs, render: "html" om in een browser te starten, waitForSelector, en returnCookies.

Stroom van één Web Unlocker-aanvraag: tot drie TLS-pogingen op verschillende exit-IP's en browservingerafdrukken, dan een headless browser als een JavaScript-uitdaging verschijnt, dan of de pagina of een gelabelde blokkade
Eén GET via de unlocker: eerst TLS-pogingen, een echte browser alleen wanneer een JavaScript-uitdaging verschijnt, en een gelabeld resultaat in beide gevallen.

Zie hoe de Web Unlocker omgaat met geblokkeerde aanvragen

Stap 4: lees de x-qp headers en handel een gerapporteerde blokkade af

Via de forward proxy rapporteert de unlocker over de respons zelf. Dit zijn de headers die het waard zijn om bij elke oproep te loggen:

HeaderWat het je vertelt
x-qp-unlocker-attemptsHoeveel pogingen de aanvraag kostte, inclusief herhalingen
x-qp-unlocker-rendered / x-qp-unlocker-escalatedIngesteld op 1 wanneer een headless browser de pagina ophaalde / wanneer de aanvraag opklom van de TLS-laag
x-qp-unlocker-profileWelke browservingerafdruk de geretourneerde poging gebruikte
x-qp-clearanceWat er gebeurde met de browser clearance cache bij deze oproep: minted, hit of miss
x-qp-session / x-qp-countryDe exit-sessie en het land dat daadwerkelijk werd gebruikt
x-qp-unlocker-blockedAlleen aanwezig wanneer de pagina nog steeds geblokkeerd was na elke poging
x-qp-block-class / x-qp-vendorWelk soort muur het was, en welke anti-bot leverancier, wanneer herkend

Een pagina die nog steeds geblokkeerd is, komt nooit aan als een stille 200. Als de oorsprong weigerde met zijn eigen fout (403, 429, 503), wordt die status ongewijzigd doorgegeven met x-qp-unlocker-blocked ingesteld. Als de oorsprong een uitdagingpagina met een 2xx serveerde, verandert de proxy het in een 502 en plaatst de code van de oorsprong in x-qp-origin-status. Dus één controle dekt beide:

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

Stap 5: de Node.js-versie (web unlocker API in JavaScript)

In Node neemt fetch een dispatcher. Gebruik ProxyAgent van undici voor de forward proxy, en importeer fetch uit hetzelfde pakket zodat de twee versies overeenkomen. De ProxyAgent-documentatie vermeldt de hier gebruikte opties: token voor de Proxy-Authorization waarde en requestTls voor de TLS-instellingen naar het doel.

// 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);
}

Het REST-eindpunt heeft niets nodig buiten de fetch ingebouwd in 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);

Op axios is het gebruikelijke patroon voor HTTPS-doelen achter een proxy om een agent van https-proxy-agent door te geven als httpsAgent en proxy: false in te stellen, of om het REST-eindpunt hierboven aan te roepen en het proxy-protocol helemaal over te slaan.

Stap 6: wanneer je de unlocker niet nodig hebt

De unlocker wordt per GB in rekening gebracht op elke byte die hij verplaatst, inclusief herhalingen en browserpagina-ladingen. Het sturen van pagina's die een gewone aanvraag prima ophaalt, verbrandt saldo voor niets. In onze run kwamen deze prima terug zonder enige hulp:

SiteGewoon, datacenter serverGewoon, residentiële proxyWeb Unlocker
docs.python.org (urllib.request pagina)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 (scrapy repository)200200200, 2,0 s
bbc.com (Technologie)302 redirect302 redirect200, 1,4 s

De BBC-rij is geen blokkade. Onze testclient volgde geen redirects, en de 302 is de site die je naar een andere URL stuurt. requests volgt standaard redirects en landt op de pagina.

Dus het juiste patroon is eerst gewoon, unlocker bij een gedetecteerde blokkade. Detectie vereist zorg. De markeringsscan in onze eigen testharnas markeerde de Wikipedia- en GitHub-pagina's als geblokkeerd omdat hun bodies het woord 'captcha' bevatten, hoewel beide de echte pagina met een 200 terugstuurden. Controleer de statuscode, Cloudflare's cf-mitigated header en de paginatitel, niet de hele body:

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])

De except tak is belangrijk. De laptopcategorie van Best Buy gaf onze gewone clients nooit een HTTP-reactie: de verbinding mislukte zowel vanaf de datacenter server als via de residentiële proxy. Via de unlocker gaf dezelfde URL 200 bij de eerste poging in 6,3 seconden. Een scraper die alleen statuscodes controleert, zou die nooit escaleren.

Gemeten resultaten: vier sites die de unlocker nodig hadden

Gemeten op 29 september 2026. Gewone aanvragen waren curl met een Chrome User-Agent, eenmaal vanaf een Frankfurt datacenter server en eenmaal via een residentiële proxy met een Amerikaanse exit. Web Unlocker-aanvragen gebruikten de residentiële laag met een Amerikaanse exit. Tijden zijn wandklok voor een enkele aanvraag.

SiteGewoon, datacenter serverGewoon, residentiële proxyWeb Unlocker
Indeed (vacaturezoekopdracht)403, 'Security Check' pagina403, 'Enable JavaScript and cookies to continue'200, 1 poging, 2,6 s
Glassdoor (bedrijfsbeoordelingen)403, 'Security' pagina met captcha403, 'Enable JavaScript and cookies to continue'200, 2 pogingen + browser, 17,2 s
Walmart (zoekopdracht)307 redirect, geen resultaatpagina307 redirect, geen resultaatpagina200, 1 poging, 4,8 s
Best Buy (laptopcategorie)Verbinding mislukt, geen reactieVerbinding mislukt, geen reactie200, 1 poging, 6,3 s

Kortom: Indeed, Walmart en Best Buy slaagden bij de eerste TLS-poging zonder browser. Glassdoor had het volledige pad nodig, twee TLS-pogingen en daarna een headless browser om de JavaScript-uitdaging te beantwoorden, daarom duurde het 17,2 seconden. Voor Walmart hebben we niet opgenomen waar de 307 naar wees. Wat we wel kunnen zeggen is dat geen van de gewone clients de resultaatpagina ontving en de unlocker wel.

Herhaalde aanvragen naar een uitdaging-beschermde site worden sneller wanneer je een sessie vastpint. Stuur dezelfde x-qp-session waarde, of houd dezelfde -session-<id> in de proxy-gebruikersnaam, en de clearance-cookie verdiend door de browser wordt hergebruikt. In een Glassdoor-test op 29 september 2026, met de sessie vastgepind in de gebruikersnaam en twee aparte verbindingen, duurde de eerste aanvraag 14,0 seconden via de browser en de tweede 1,4 seconden, met x-qp-clearance: hit. Onze gids voor het scrapen van Indeed-vacatureplaatsingen behandelt wat te doen met de pagina zodra je deze hebt.

Beslissingsschema voor een scraper: probeer eerst een gewone aanvraag, en stuur de aanvraag alleen door de web unlocker wanneer de status, de cf-mitigated header of de paginatitel een blokkade toont
Eerst gewoon, unlocker bij een gedetecteerde blokkade. De unlocker rekent elke byte die hij verplaatst, dus de goedkoopste aanvraag is degene die zonder werkt.

Als je hier bent gekomen vanuit een 403 die alleen buiten de browser gebeurt, gaat onze uitleg over waarom curl een 403 krijgt wanneer de browser werkt dieper in op de diagnose. Voor een site-per-site overzicht van welke laag je daadwerkelijk nodig hebt, zie web unlocker vs residentiële proxy, getest.

Veelgestelde vragen

Is er een gratis web unlocker API?

De open-source onderdelen zijn gratis: requests, curl_cffi voor browserachtige TLS, en Playwright voor een echte browser. Ze goed laten draaien is het werk, omdat je ook schone exit-IP's en een browser voor JavaScript-uitdagingen nodig hebt. Op QuantumProxies.io is het aanmaken van een account gratis, en unlocker-verkeer wordt per GB in rekening gebracht van een voorafbetaald saldo dat je in het dashboard aanvult.

Kun je Glassdoor of Zillow scrapen met Python?

In onze test van 29 september 2026 kregen gewone aanvragen naar een Glassdoor-beoordelingspagina een 403 van zowel een datacenter server als een residentiële proxy, en de unlocker retourneerde de pagina na een browser-escalatie. Zillow blokkeerde de datacenter server maar leverde de pagina via een gewone residentiële proxy. Controleer de voorwaarden van elke site voordat je iets verzamelt.

Kan webscraping worden gedetecteerd?

Ja. Sites scoren het netwerktype en de geschiedenis van het IP, de TLS-handshake, de HTTP/2-instellingen, de volgorde van headers en, op uitdagingpagina's, wat JavaScript rapporteert over de browser. Een gewone Python-aanvraag faalt op meerdere van die punten tegelijk. Daarom helpt het zelden om alleen de User-Agent te veranderen, en waarom de unlocker het IP, de vingerafdruk en, indien nodig, de hele client verandert.

Moet ik een certificaat installeren om de forward proxy te gebruiken?

Alleen voor HTTPS via de klassieke forward proxy, omdat de unlocker TLS beëindigt om de aanvraag te herschrijven. Ofwel sla verificatie over voor die sessie (verify=False in Python, -k in curl) of download de CA van /api/v1/scraper/unlock/ca met je API-sleutel en vertrouw het. Het REST-eindpunt heeft helemaal geen certificaat nodig.

Hoe houd ik hetzelfde IP voor meerdere aanvragen?

Stuur dezelfde x-qp-session headerwaarde bij elke aanvraag via de forward proxy, of plaats dezelfde -session-<id> in de proxy-gebruikersnaam. Oproepen die die sessie delen vertrekken vanaf dezelfde exit, en een clearance-cookie verdiend door de browser wordt hergebruikt. Op het REST-eindpunt, geef een sessionId door met rotatie ingesteld op sticky. Een sticky sessie kan duren van 3 tot 1.440 minuten.

Het hele patroon past in één zin: haal gewoon op, detecteer een blokkade op status, header en titel, stuur alleen die URL's door de unlocker, en log de x-qp headers zodat je weet wat elke pagina je heeft gekost. Begin met de residentiële laag, en houd de Mobiele laag voor doelen die ook residentiële reeksen weigeren.

Probeer de Web Unlocker op residentiële exits