curl devuelve 403 pero el navegador funciona: Encuentra la pieza que falta
El navegador lo carga. curl obtiene 403. La brecha entre esas dos solicitudes siempre es finita y siempre se puede encontrar: aquí te mostramos cómo dividirla en cinco minutos.
Pegas una URL en Chrome y la página se carga. Pegas la misma URL en curl y obtienes 403 Prohibido. Nada sobre el recurso cambió entre esos dos segundos, por lo que la diferencia está completamente en la solicitud, y una solicitud es algo finito e inspeccionable. Esta guía es un procedimiento de bisección: reproduce exactamente lo que envió el navegador, luego elimina piezas hasta que el 403 regrese. Lo que eliminaste por última vez es tu respuesta. Los sospechosos, en el orden en que suelen ser culpables, son User-Agent, Referer, cookies, huella TLS y JavaScript.
Paso 0: prueba que las solicitudes realmente son diferentes
Antes de teorizar, mira lo que curl realmente envía. Con -v ves la línea de solicitud, cada encabezado y el apretón de manos TLS. Una solicitud curl por defecto es sorprendentemente delgada: típicamente Host, User-Agent: curl/8.x y Accept: */*. Un navegador envía una docena más.
# what you send, what you get back, and the TLS details
curl -v -o /dev/null https://target.example/page
# just the response headers, quickly
curl -sS -o /dev/null -D - https://target.example/page
Lee los encabezados de respuesta tan cuidadosamente como la línea de estado. Uno de ellos resuelve la cuestión de inmediato en el caso más común: Vary: User-Agent significa que el servidor sirve deliberadamente respuestas diferentes dependiendo de quién dices ser. En un caso bien documentado de Stack Overflow, curl -f contra un host Apache 2.4.38 devolvió 403 mientras que wget obtuvo el archivo idéntico con un 200, y la respuesta exitosa llevaba exactamente ese encabezado Vary: User-Agent. Pasar -A 'Wget/1.21.2' a curl lo solucionó instantáneamente. El propietario del sitio había puesto en la lista negra el agente de usuario de curl después de abusos; nada más sobre la solicitud importaba.
Mientras lees la salida: curl: (22) La URL solicitada devolvió error: 403 no es un problema separado. El código de salida 22 es lo que -f/--fail hace con cualquier error HTTP: la bandera suprime el cuerpo y falla el comando. Elimina temporalmente -f para que puedas leer realmente la página de bloqueo, que usualmente nombra el sistema que te detuvo.
Paso 1: Copiar como cURL, la respuesta de 30 segundos
Ambos navegadores principales pueden darte la solicitud exacta que acaban de hacer. Abre DevTools, ve a la pestaña de Red, haz clic derecho en la solicitud y elige "Copiar como cURL". Chrome ha enviado esto desde la versión 26 y Firefox desde la 31, y la salida incluye cada encabezado, cada cookie y el referer. Pégalo en tu terminal: si devuelve 200, tu problema está definitivamente en la forma de la solicitud, y el paso 2 encuentra qué parte.
Un detalle que desperdicia mucho tiempo aquí. Si la URL redirige, el panel de Red se borra en la navegación y copias la solicitud incorrecta. Marca "Preservar registro" en Chrome o "Registros Persistentes" en Firefox primero, para que puedas ver tanto la solicitud que redirigió como la que finalmente sirvió contenido. Las cadenas de redirección importan: en un hilo bien conocido de Unix Stack Exchange, el servidor verificó el Referer, luego rebotó a través de un 302 a una ubicación que no verificó nada en absoluto, lo que hizo que el fallo pareciera aleatorio hasta que toda la cadena fue visible.

Paso 2: divide los encabezados
Comienza desde el comando "Copiar como cURL" que funciona y elimina encabezados uno por uno, ejecutando de nuevo después de cada eliminación. La primera eliminación que trae de vuelta el 403 nombra a tu culpable. En la práctica, casi siempre es uno de cuatro.
curl -sS -o /dev/null -w '%{http_code}\n' \
-A 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' \
-H 'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' \
-H 'Accept-Language: en-GB,en;q=0.9' \
-e 'https://target.example/' \
-b 'session=abc123; consent=1' \
-L \
'https://target.example/page'
- User-Agent (
-A) — bloqueado directamente, o el servidor se bifurca en él. Prueba una cadena actual de Chrome, luego la de wget, luego una sin sentido; el patrón de resultados te dice si estás golpeando una lista negra o una lista de permitidos. - Referer (
-e) — los activos y enlaces de descarga a menudo devuelven 403 a menos que la solicitud parezca que proviene de la propia página del sitio. El encabezado es opcional por especificación, que es exactamente por qué la gente lo olvida. - Cookies (
-b) — una cookie de consentimiento, sesión o anti-bot establecida en una página anterior. Confírmalo en segundos: abre la URL en una ventana privada. Si el navegador también da 403 allí, las cookies son tu respuesta. - Authorization (
-u, o un encabezado de portador) — las URLs firmadas o tokenizadas frecuentemente dan 403 cuando se copian fuera de contexto, porque el token estaba vinculado a una sesión o ya ha expirado.
Dos detalles finales para este nivel. Cita la URL: una cadena de consulta que contiene & o un token de acceso se desordena por tu shell de otra manera, y el 403 resultante no tiene nada que ver con el servidor. Y si estás depurando desde PHP o Node en lugar de la shell, replica el mismo conjunto de encabezados allí: los valores predeterminados de libcurl dentro de PHP difieren de los de la herramienta de línea de comandos, que es por qué la solicitud idéntica puede pasar en un terminal y fallar en el código. Nuestro recetario de proxies curl cubre la sintaxis de las banderas en su totalidad.
Paso 3: cuando los encabezados idénticos aún devuelven 403
Si una copia byte por byte de los encabezados del navegador aún falla, la decisión se tomó antes de que tus encabezados fueran analizados. Dos capas se encuentran debajo de ellos.
Huella TLS. Tu ClientHello — suites de cifrado, extensiones, preferencias de curva, ALPN, más el marco de configuración HTTP/2 que sigue — se resume en un valor JA3 o JA4. curl construido contra OpenSSL produce uno que ningún navegador produce jamás, y los sistemas anti-bot lo comparan con tu User-Agent declarado. Afirmar ser Chrome mientras haces el apretón de manos como OpenSSL es una contradicción que están diseñados para detectar. La solución es un cliente que reproduzca los apretones de manos del navegador: curl-impersonate en la línea de comandos, o curl_cffi desde Python.
# pip install curl_cffi
from curl_cffi import requests
proxy = "http://USER:PASS@gate.quantumproxies.io:8000"
r = requests.get(
"https://target.example/page",
impersonate="chrome", # browser ClientHello + HTTP/2 settings
proxies={"http": proxy, "https": proxy},
timeout=20,
)
print(r.status_code, r.headers.get("content-type"))
Tu IP. El navegador que funciona suele estar en tu conexión doméstica mientras curl se ejecuta en un VPS. Los ASN de hosting están publicados y preevaluados, por lo que la misma solicitud desde una dirección residencial se juzga de manera diferente antes de que siquiera se lea. Cambiar la salida es un cambio de una línea con proxies residenciales — más de 90 millones de IPs en más de 200 países, HTTP y SOCKS5 en cada plan — y es la forma más rápida de descartar la red. La mecánica de la capa de huellas está en huellas TLS JA3/JA4.
Descarta la red con proxies residenciales

Paso 4: la página necesita un navegador, no un cliente
A veces el 403 no es un juicio sobre ti en absoluto: es el modo de fallo de un desafío que nunca intentaste. Una discusión pública en GitHub sobre verificadores de enlaces que golpean npmjs.com lo dice claramente: curl no puede producir una solución de desafío válida, por lo que la solicitud se bloquea con un 403. El servidor emite un pequeño problema de JavaScript, espera un momento por la respuesta y rechaza cualquier cosa que no pueda ejecutarlo. Ningún conjunto de encabezados, ninguna huella y ninguna IP pasa una prueba que requiere ejecutar código.
En ese punto tienes tres opciones honestas: manejar un navegador real y pagar el costo, encontrar el endpoint JSON que la página misma llama (a menudo sentado en la misma pestaña de Red que ya tienes abierta), o entregar la URL a un servicio que renderiza bajo demanda. La Scraper API de QuantumProxies hace lo último: TLS de grado navegador, salidas residenciales, renderizado de JavaScript solo donde una página lo necesita, y markdown, JSON o HTML sin procesar de vuelta de una solicitud. Si lo que obtienes es una página vacía en lugar de una prohibida, ese es un diagnóstico diferente: ve a por qué tu scraper devuelve una página vacía. Y si la página de bloqueo lleva un Cloudflare Ray ID, ve a Cloudflare error 1020 en su lugar.
Preguntas frecuentes
¿Por qué curl obtiene 403 cuando mi navegador no?
Porque curl envía aproximadamente tres encabezados, sin cookies, sin referer y una huella TLS no de navegador, mientras que tu navegador envía una docena de encabezados, un contenedor de cookies y un apretón de manos de Chrome. El servidor está rechazando la solicitud, no el recurso. Reproduce la solicitud exacta del navegador con "Copiar como cURL", luego elimina encabezados uno por uno para encontrar qué diferencia importa.
¿Por qué wget tiene éxito donde curl obtiene 403?
Casi siempre el User-Agent. Algunos servidores ponen en la lista negra específicamente el UA de curl después de abusos mientras dejan el de wget solo: un caso documentado mostró un encabezado de respuesta Vary: User-Agent confirmando que el servidor se bifurca en él, y curl -A 'Wget/1.21.2' restauró el 200. wget también envía Accept-Encoding y Connection por defecto, lo que ocasionalmente también importa.
¿Cómo configuro un User-Agent en curl?
Usa -A 'cadena', o el equivalente -H 'User-Agent: cadena'. Prefiere una cadena completa y actual de navegador sobre un Mozilla/5.0 truncado, que algunos servidores ahora rechazan precisamente porque ningún navegador real envía solo dos tokens. Empareja con valores coincidentes de Accept y Accept-Language para que todo el conjunto se mantenga coherente.
¿Qué significa el error 22 de curl?
El código de salida 22 es producido por -f/--fail siempre que el servidor devuelve un error HTTP, y el mensaje cita el estado, comúnmente 403. Es una bandera de reporte, no una falla distinta. Elimina -f para ver el cuerpo de la respuesta, que usualmente explica el bloqueo mucho mejor que el código de salida.
¿Puede un proxy solucionar un curl 403?
Soluciona el subconjunto causado por la reputación o geografía de IP: un gran subconjunto cuando tu script se ejecuta en un host en la nube y tu navegador no. No solucionará un referer faltante, una cookie ausente o un desafío de JavaScript. Prueba los encabezados primero, ya que no cuestan nada, luego cambia la IP de salida para aislar la capa de red.
No hay misterio aquí, solo una brecha: el navegador envió una solicitud y tú enviaste otra. Copia la del navegador, redúcela hasta que se rompa, y siempre encontrarás la pieza que importaba: usualmente un encabezado, a veces una huella, ocasionalmente un desafío que necesita un navegador real para responder.