Back to Blog

Python GET Request 403 Error: Why It Happens and How to Fix It

Chloe Sun

Aug 17, 2026 · Troubleshooting · 6 min read

TL;DR

A 403 means the server understood the request but refused it; it does not automatically mean Python is broken. First compare the exact URL, method, credentials, headers, cookies, redirects, and source network with an authorized request that works. Then inspect the response body and request history before changing one variable at a time. If the response is a browser challenge or an access-policy denial, use the provider’s API, export, or other approved integration instead of attempting to bypass it.

Important: Do not use these techniques to bypass authentication, access controls, anti-bot challenges, or IP restrictions without authorization.

What Does HTTP 403 Forbidden Mean?

Web Scraping Blocked by 403 Forbidden Heres Whats Happening

HTTP 403 is a client-error status code. RFC 9110 describes it as a response in which the server understood the request but refuses to fulfill it. The cause may be permissions, authentication state, application rules, network policy, or a security service. A valid requests.get() call can therefore receive 403 even when the Python syntax is correct.

Do not start by randomly changing the User-Agent. Capture the response, identify the server’s reason, and compare the failed request with a known-working request that you are authorized to reproduce.

Why Python Requests Gets 403 When a Browser Works

Browsers accumulate cookies, authentication state, redirect history, navigation headers, and JavaScript-generated values. A one-off Python call sends only the data you provide and does not inherit your browser session. The two clients can also use different endpoints, query strings, IP addresses, TLS settings, or network policies.

The useful question is not whether one client is “human” and the other is “bot.” It is which request state differs and which difference explains the server’s decision.

How to Fix a Python GET Request 403 Error

Use this order so that each test changes one meaningful variable:

  1. Inspect status_code, response headers, body, URL, and redirect history.
  2. Verify the exact URL, query parameters, method, and endpoint type.
  3. Check documented authentication and account permissions.
  4. Add only headers the application actually requires.
  5. Use requests.Session() when cookies must persist.
  6. Review Referer and redirect requirements.
  7. Respect rate limits and investigate IP or network policy.
  8. Identify browser challenges and switch to an approved access method.

1. Verify the Exact URL and Endpoint

A public webpage and its API endpoint may have different permissions. Confirm the scheme, host, path, query string, method, and parameters. Passing query values through params avoids accidental encoding errors:

import requests

url = "https://example.com/products"
params = {"category": "books", "page": 1}
response = requests.get(url, params=params, timeout=20)

print(response.url)
print(response.status_code)

Compare these values with the authorized browser or API request. If an official API exists, use its documented endpoint and credentials.

2. Check Required Headers

Some services require Accept, a content type, or an application-specific header. Follow the provider’s documentation; a User-Agent change is not a universal fix. A descriptive application identity is usually better than pretending to be a browser:

import requests

headers = {
    "Accept": "application/json",
    "User-Agent": "inventory-monitor/1.0 (contact: ops@example.com)",
}
response = requests.get("https://api.example.com/items", headers=headers, timeout=20)
response.raise_for_status()
data = response.json()

For a reference on constructing and inspecting request headers, see Python requests headers.

3. Persist Cookies with a Session

Multi-step workflows often require a cookie set by an earlier response. Session stores cookies and common headers for subsequent requests:

import requests

with requests.Session() as session:
    session.headers.update({"User-Agent": "authorized-client/1.0"})
    landing = session.get("https://example.com/start", timeout=20)
    landing.raise_for_status()
    result = session.get("https://example.com/account", timeout=20)
    print(result.status_code, result.url)

Use only cookies belonging to an account and workflow you are authorized to access. Never commit real session tokens.

4. Handle Authentication Correctly

A 403 can mean that supplied credentials lack permission, while 401 generally indicates missing or invalid authentication. Follow the API’s documented scheme and keep secrets outside source control:

import os
import requests

token = os.environ["EXAMPLE_API_TOKEN"]
headers = {"Authorization": f"Bearer {token}"}
response = requests.get("https://api.example.com/profile", headers=headers, timeout=20)
print(response.status_code, response.text[:300])

Do not copy another person’s cookies, tokens, or account state into a script.

5. Add Referer Only When Required

Some applications validate navigation context or CSRF-related headers. Send the documented value only when the workflow requires it:

import requests

headers = {"Referer": "https://example.com/catalog"}
response = requests.get("https://example.com/catalog/item-1", headers=headers, timeout=20)
print(response.status_code)

A Referer cannot grant permission or solve a blocked IP, rate limit, or anti-bot challenge.

Python-referer

6. Inspect Redirects

Requests follows GET redirects by default. Compare the final URL and each hop; a browser may reach a login, consent, regional, or canonical URL that Python does not:

import requests

response = requests.get("https://example.com/private", timeout=20, allow_redirects=True)
print("Final URL:", response.url)
for hop in response.history:
    print(hop.status_code, hop.headers.get("Location"))

debug = requests.get("https://example.com/private", timeout=20, allow_redirects=False)
print("Initial response:", debug.status_code, debug.headers.get("Location"))

7. Respect Rate Limits

Follow published quotas, reduce concurrency, and use the provider’s recommended backoff for 429 responses. A bounded retry loop is appropriate for transient throttling, not for defeating a persistent 403:

import time
import requests

for attempt in range(3):
    response = requests.get("https://api.example.com/items", timeout=20)
    if response.status_code != 429:
        break
    time.sleep(2 ** attempt)
print(response.status_code)

8. Check IP and Network Restrictions

Corporate networks, cloud ranges, geographic rules, and account policies can all produce 403. Confirm that your source network is allowed and contact the service operator when it is not. For an authorized proxy workflow, follow Python proxy integration and document the approved endpoint and network policy; do not use a proxy to defeat an access restriction.

9. Recognize Cloudflare and Other Browser Challenges

cloudflare
Security services may require JavaScript, a challenge page, or an interactive verification step. requests does not execute page JavaScript, so adding arbitrary headers may leave the challenge unchanged. Check the site’s API, export, feed, terms, and automation policy. For a site you own or are explicitly authorized to test, use browser automation within its security configuration. Cloudflare’s challenge documentation explains the supported mechanisms.

10. Debug the Actual Response

Inspect the body and the prepared request before making further changes:

import requests

response = requests.get("https://example.com/resource", timeout=20)
print("Status:", response.status_code)
print("URL:", response.url)
print("History:", [(r.status_code, r.url) for r in response.history])
print("Response headers:", dict(response.headers))
print("Sent headers:", dict(response.request.headers))
print("Body preview:", response.text[:500])

Messages such as “missing token,” “access denied,” or “challenge required” point to different next steps.

Practical Decision Tree

Observation Next step
Wrong URL or method Correct the endpoint and parameters
401 Fix the documented authentication
Permission message Check account scope and authorization
Missing-token message Supply the required credential
403 after many requests Check quota and back off
IP or network message Review policy or contact the operator
Challenge page Use an approved API or browser flow
Browser works, API fails Compare endpoint and credentials
cURL works, Python fails Compare the actual prepared requests

Clean Diagnostic Example

This complete example records the evidence needed for a support ticket without attempting to bypass controls:

import requests

url = "https://example.com/resource"
headers = {"Accept": "application/json", "User-Agent": "authorized-client/1.0"}

with requests.Session() as session:
    response = session.get(url, headers=headers, timeout=20, allow_redirects=True)
    print("Status:", response.status_code)
    print("Final URL:", response.url)
    print("Redirects:", [(r.status_code, r.headers.get("Location")) for r in response.history])
    print("Body preview:", response.text[:500])

When an approved proxy integration is involved, verify the documented proxy parameters and, where applicable, the API whitelist setup. If the proxy connection itself fails, consult the provider’s proxy API connection failed troubleshooting page.

Python Requests Gets 403 but cURL Works

Compare the exact URL, method, query parameters, headers, cookies, authentication, redirects, proxy, source network, and response body. Inspect what cURL actually sends and compare it with response.request.headers and response.history in Python. Do not copy credentials from an unauthorized account merely to make the clients match.

Python Requests Gets 403 but cURL Works

Summary: Common Causes

Cause Appropriate response
Wrong endpoint Compare exact URLs and methods
Missing headers Add documented headers only
Missing session state Use requests.Session()
Authentication or permission Follow the supported account or API flow
Redirect mismatch Inspect response.history and response.url
Rate limit Follow quotas and back off
IP or network rule Check policy or contact the operator
Anti-bot challenge Use an official or otherwise permitted method

Frequently Asked Questions

Ready to start collecting data at scale?

Try for Free