403 Prohibido en Web Scraping: La Escalera de Soluciones Que Realmente Funciona
Un 403 no es un problema de permisos, es un problema de detección. Cuatro peldaños separan un script bloqueado de un 200, y la mayoría de las personas se detienen en el primero.
Un error 403 prohibido en web scraping casi nunca significa lo que dice el código de estado. HTTP 403 se define como el servidor entendiendo tu solicitud y negándose a autorizarla, pero cuando llega a un scraper, rara vez tiene que ver con permisos o un inicio de sesión faltante. Significa que el sitio miró tu solicitud, decidió que una máquina la envió y cerró la puerta. Eso te dice qué cambiar: no tus credenciales, sino la forma de tu tráfico. A continuación se muestra la escalera de soluciones, el peldaño más barato primero, con las verificaciones que identifican en cuál peldaño estás atascado.
403 vs 401 vs 429: lo que cada uno te está diciendo
Obtén el diagnóstico correcto antes de escribir código. Un 401 No Autorizado pide credenciales — proporcionarlas lo soluciona. Un 403 Prohibido se niega independientemente de las credenciales, por lo que iniciar sesión no cambia nada si la detección de bots lo activó. Un 429 Demasiadas Solicitudes se trata de volumen y se limpia cuando la ventana se restablece; un 403 se trata de identidad y persiste hasta que cambies el aspecto de tu solicitud. Si tu scraper está obteniendo 429 en lugar de 403, la cura es el ritmo, no el disfraz — cubrimos eso en solucionar 429 demasiadas solicitudes.
Diagnostica en 60 segundos, antes de cambiar cualquier código
Tres comandos te dicen casi todo. Ejecuta la solicitud básica, ejecútala de nuevo solo con un User-Agent de navegador cambiado, y luego lee el cuerpo de la respuesta — la razón del bloqueo generalmente está escrita en él.
# 1. Bare request: what does the target give a naked client?
curl -sS -o /dev/null -w '%{http_code}\n' https://target.example/page
# 2. Same request, browser User-Agent only
curl -sS -o /dev/null -w '%{http_code}\n' \
-A 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' \
https://target.example/page
# 3. Read the body and the response headers - the reason is in there
curl -sS -D - https://target.example/page | head -c 600
Interprétalo así. Si el paso 2 devuelve 200, todo el problema son los encabezados y terminas en el peldaño 1. Si el cuerpo menciona Cloudflare, un Ray ID o Error 1020, estás detrás de una regla WAF — ver Cloudflare error 1020. Una página Prohibida de Apache o nginx generalmente significa un módulo del servidor como mod_security, que ha bloqueado User-Agents de bots conocidos desde mucho antes de que existiera la gestión moderna de bots. Y si un navegador en la misma máquina carga la página mientras tu cliente no, trabaja a través de curl 403 pero el navegador funciona.

Peldaño 1: deja de anunciarte en los encabezados
urllib de Python se identifica como algo como python-urllib/3.3.0; requests envía python-requests/2.x. Esas cadenas son una confesión, y la respuesta más votada en el hilo canónico de Stack Overflow sobre 403s en scraping con Python es simplemente: envía un User-Agent de navegador en su lugar. Eso aún funciona en muchos sitios. Pero dos cosas han cambiado desde que se escribió esa respuesta. Primero, un Mozilla/5.0 simple ahora es en sí mismo una bandera — comentaristas en ese mismo hilo informan que los sitios lo bloquean directamente, porque ningún navegador real envía un UA de dos tokens. Segundo, los servidores modernos comparan todo tu conjunto de encabezados, no un solo campo.
Envía un conjunto coherente: un UA de navegador actual, la cadena Accept correspondiente, un idioma, y los encabezados de metadatos Sec-Fetch-* que Chromium añade a cada navegación. El orden de los encabezados también importa en objetivos más estrictos — usa un mapeo ordenado y ponlos en la secuencia que usa un navegador.
import requests
HEADERS = {
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
),
"Accept": (
"text/html,application/xhtml+xml,application/xml;q=0.9,"
"image/avif,image/webp,*/*;q=0.8"
),
"Accept-Language": "en-GB,en;q=0.9",
"Accept-Encoding": "gzip, deflate", # add 'br' only if brotli is installed
"Upgrade-Insecure-Requests": "1",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Connection": "keep-alive",
}
with requests.Session() as s:
s.headers.update(HEADERS)
r = s.get("https://target.example/page", timeout=20)
print(r.status_code, len(r.content))
Una trampa de coherencia atrapa a casi todos: un UA que afirma ser un Chrome de escritorio de EE. UU., emparejado con Accept-Language: de-DE y una IP de salida en Brasil, es una discrepancia que cualquier sistema decente nota. Mantén el user agent, el idioma y la geografía de la IP contando la misma historia.
Algunos 403s en este peldaño son aún más simples: un Referer faltante. Los servidores que solo sirven un recurso cuando la solicitud parece provenir de su propia página devolverán 403 a un acceso directo y 200 en el momento en que añadas la URL de referencia. Es un clásico, y cuesta un encabezado probarlo.
Peldaño 2: la huella digital TLS que los encabezados no pueden arreglar
Si los encabezados perfectos aún devuelven 403, el bloqueo ocurrió antes de que tus encabezados fueran siquiera leídos. Cada cliente HTTPS anuncia sus suites de cifrado, extensiones, curvas elípticas y ALPN en el TLS ClientHello, y esa combinación se convierte en una huella digital JA3 o JA4. requests de Python, net/http de Go y curl estándar tienen cada uno una distintiva que ningún proveedor anti-bot tiene problemas para distinguir de Chrome. Afirmar ser Chrome 131 en un encabezado mientras se realiza el handshake como OpenSSL es la contradicción más fuerte que un scraper puede hacer.
La solución es un cliente que imita un navegador real en la capa TLS. En Python eso es curl_cffi, un enlace a un libcurl parcheado que reproduce ClientHellos de navegador:
# pip install curl_cffi
from curl_cffi import requests as cffi
proxy = "http://USER:PASS@gate.quantumproxies.io:8000"
r = cffi.get(
"https://target.example/page",
impersonate="chrome", # Chrome JA3/JA4 + HTTP/2 settings
proxies={"http": proxy, "https": proxy},
timeout=20,
)
print(r.status_code)
Node tiene equivalentes construidos sobre las mismas pilas TLS parcheadas. Si deseas conocer la mecánica completa de por qué este único cambio convierte un 403 en un 200 en sitios protegidos, lee cómo la huella digital JA3/JA4 delata tu scraper.
Peldaño 3: la IP es el mensaje
Los encabezados y TLS describen al cliente. La IP describe quién está preguntando, y tiene un peso considerable. Las direcciones en rangos de hosting y nube están publicadas, son fáciles de mapear por ASN, y tienen una puntuación de confianza más baja antes de que se inspeccione un solo byte de tu solicitud — por eso un scraper en un VPS obtiene 403s que el mismo código en una conexión doméstica pasa sin problemas. Las direcciones residenciales pertenecen a proveedores de internet para consumidores y se tratan como personas; las IPs de operadores móviles están detrás de CGNAT con miles de suscriptores reales cada una, lo que las hace las más difíciles de bloquear al por mayor.
Así que el peldaño 3 es un cambio, no una reescritura: envía la misma solicitud bien formada desde una salida residencial. QuantumProxies maneja más de 90 millones de IPs residenciales en más de 200 países con rotación por solicitud o sesiones persistentes, HTTP y SOCKS5 en cada plan, y facturación por GB — una línea de configuración cambia el ASN que tu objetivo ve. ¿Ya estás en residencial y aún bloqueado? Verifica la reputación del pool con nuestro verificador de puntuación de calidad de IP gratuito: cualquier cosa con una puntuación superior a 75 está quemada y recogerá 403s sin importar qué tan buenos sean tus encabezados.
Sáltate la escalera: obtén cualquier página con la Scraper API

Peldaño 4: renderizado y desafíos
El último peldaño es el caro. Algunos 403s son la mitad visible de un desafío JavaScript: el servidor envía un pequeño script, espera una respuesta en segundos, y rechaza todo lo que no pueda ejecutarlo. Ningún conjunto de encabezados y ninguna IP arreglan eso, porque la prueba es si puedes ejecutar código. Opciones en costo ascendente: un navegador sin cabeza con un conjunto de parches de sigilo, un pool de navegadores gestionado, o una API de scraping que renderiza bajo demanda. El renderizado cuesta muchas veces más por página que una solicitud HTTP simple, así que escala solo para las URLs que realmente lo necesiten.
La Scraper API de QuantumProxies colapsa los peldaños 2 a 4 en una sola solicitud: TLS de grado de navegador, salidas residenciales, renderizado de JavaScript cuando una página lo necesita, y markdown, JSON o HTML bruto de vuelta. Ese es el intercambio honesto — dejas de mantener la escalera y pagas por página exitosa en su lugar.
Trabajando la escalera en la práctica
- Vuelve a probar después de cada cambio. Dos cambios a la vez y no aprendes nada sobre cuál funcionó.
- Guarda el cuerpo 403 bruto durante el desarrollo. Las páginas de bloqueo llevan Ray IDs, nombres de reglas y huellas digitales de proveedores que nombran a tu oponente.
- Trata un 403 en la primera solicitud como un problema de identidad; un 403 que aparece después de 200 páginas buenas es un problema de reputación o ritmo.
- Reintenta un 403 como máximo una vez, y solo después de cambiar algo. Martillar la misma solicitud desde la misma IP convierte un bloqueo suave en uno duro.
- Si existe una API pública o feed para los mismos datos, úsala. Es más barato que cada peldaño anterior y nunca se rompe en un rediseño.
La prevención de baneos es la disciplina hermana: una vez que tienes un 200, mantenerlo es cuestión de ritmo, higiene de sesión y salud del pool en lugar de disfraz.
Preguntas frecuentes
¿Qué causa un error 403 prohibido en web scraping?
Detección, en casi todos los casos. Los desencadenantes habituales son un User-Agent de biblioteca por defecto, un conjunto de encabezados incompleto o contradictorio, una IP de centro de datos con mala reputación, un ritmo de solicitudes demasiado regular, o una huella digital TLS que no coincide con el navegador que afirmas ser. Los errores de permiso genuinos existen, pero también devuelven 403 a los navegadores — prueba en uno antes de asumir.
¿Cómo eludo un error 403 prohibido en Python?
Trabaja la escalera. Añade un conjunto completo de encabezados de navegador a un requests.Session; si eso falla, cambia a un cliente que imite el TLS de navegador como curl_cffi; si eso falla, enruta a través de un proxy residencial; si la página envía un desafío JavaScript, renderízalo o usa una API de scraping. Solo escala cuando el peldaño más barato haya sido realmente probado.
¿Por qué httpx devuelve 403 cuando mi navegador no?
Por la misma razón que requests: httpx envía un conjunto de encabezados mínimo y una huella digital TLS de Python. Copia la solicitud exacta del navegador desde DevTools, reprodúcela con httpx, y el 403 generalmente desaparece — lo que te dice que la diferencia eran los encabezados. Si persiste con encabezados idénticos, el bloqueo está en la capa TLS o IP.
¿Cómo soluciono 403 prohibido en Scrapy?
Configura un DEFAULT_REQUEST_HEADERS realista más USER_AGENT, mantén ROBOTSTXT_OBEY honesto sobre lo que se te permite obtener, habilita AUTOTHROTTLE_ENABLED, y enruta las solicitudes a través de un middleware de proxy rotativo. Los reintentos de Scrapy no reintentan 403 por defecto — añádelo a RETRY_HTTP_CODES solo si rotas la IP entre intentos.
Un 403 es información, no un muro. Te dice cuál de las cuatro señales te delató, y cada peldaño de la escalera cuesta más que el anterior. Comienza por el más barato, prueba después de cada cambio, y deja de escalar en el momento en que el código de estado se convierte en 200.