How to Scrape a Website Behind a Login
Scrape website behind login walls, the right way: persist session cookies, handle CSRF tokens, log in with Playwright, hold sticky proxies, use SparkProxy API.

You can scrape a website behind a login only after you authenticate, and the real work is staying authenticated across every request that follows. Most scrapers log in once, then lose the session on request three and can't work out why. This guide shows you how to scrape website behind login walls the right way: persist session cookies, pass CSRF tokens, drive a headless login with Playwright, hold one IP with sticky proxies, and let the SparkProxy Scraping API manage the session for you. First, the rule that matters most: only scrape data you're authorized to access.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
Method 2: Headless Login With Playwright
Single-page apps build the login form in JavaScript, sign requests with values computed at runtime, and sometimes fingerprint the browser before they'll accept a submit. requests can't run any of that. A headless browser can, because it is a real browser.
Playwright drives Chromium, Firefox, or WebKit, fills the form the way a user would, and captures the resulting session. Install it first:
pip install playwright
playwright install chromium
Then log in and save the authenticated state to a file:
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://app.sparkproxy.io/login")
page.fill('input[name="email"]', "you@sparkproxy.io")
page.fill('input[name="password"]', "your-password")
page.click('button[type="submit"]')
# Wait for the post-login page so cookies are fully set before you save
page.wait_for_url("**/dashboard")
# storage_state captures cookies AND localStorage in one file
page.context.storage_state(path="auth.json")
browser.close()
storage_state is the piece most tutorials skip. It writes both cookies and localStorage to auth.json. That matters because token-based apps keep the auth token in localStorage, not a cookie, and a cookies-only export would leave you logged out. For login flows that fingerprint the browser or throw a challenge, run headed (headless=False) while you debug, and route Playwright through a proxy so the login IP matches the IP you'll scrape from later. If running browsers and proxies yourself starts to hurt, weigh the trade-off in a managed scraping API versus self-managed proxies.
Keep Sessions Alive With Sticky Proxies
Here's the failure that ends most authenticated-scraping projects: you log in successfully, scrape ten pages, and then every request starts redirecting to the login screen. The cookie is fine. The IP changed.
A rotating proxy assigns a new IP to each request by default. That's exactly what you want for stateless scraping and exactly what breaks a login session, because the server sees your valid cookie arriving from a brand-new IP on every hit and treats it as a hijacked session. The fix is a sticky session: a proxy that pins one exit IP for the full lifetime of your login.
With sticky residential or ISP proxies, you request a session that holds the same IP for a set window (commonly up to 10 to 30 minutes, sometimes longer), which is enough to log in and scrape under one consistent identity. The session is usually selected through the proxy username:
import requests
# A sticky session pins ONE exit IP for the whole login flow.
# The session token in the username tells the gateway to reuse that IP.
proxy = "http://user-session-a1b2c3:your-pass@residential.sparkproxy.io:8000"
session = requests.Session()
session.proxies = {"http": proxy, "https": proxy}
# Log in and scrape on the SAME IP so the cookie stays valid
session.post("https://app.sparkproxy.io/login",
data={"email": "you@sparkproxy.io", "password": "your-password"},
timeout=15)
session.get("https://app.sparkproxy.io/dashboard", timeout=15)
The exact username format for sticky sessions lives in your dashboard, and the mechanics of encoding a session token into proxy credentials are covered in how proxy authentication works. Two rules make sticky sessions reliable: keep the User-Agent identical between login and scraping (a User-Agent flip invalidates many sessions just like an IP flip), and give each account its own sticky session so their cookies never cross IPs.
Method 3: Authenticated Scraping With the SparkProxy API
Managing cookies, CSRF tokens, headless browsers, and sticky IPs by hand is a lot of moving parts. The SparkProxy Scraping API collapses them into request parameters. It runs a real browser, holds the proxy, and persists the session for you. See the full parameter list at the Scraping API docs.
Two parameters do the heavy lifting for authenticated work:
session_id: labels a browser profile (max 128 characters). Reuse the same value and you get the same cookies and the same proxy IP across calls, which is a sticky session with no username juggling.cookies: a JSON array of cookie objects injected before the page loads, so you can hand it a session you obtained elsewhere.
If you already have cookies from a browser login, inject them and start scraping straight away:
curl -X POST "https://scrape.sparkproxy.io/api/v1" \
-H "X-API-Key: sk-your-api-key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://app.sparkproxy.io/dashboard",
"render_js": true,
"session_id": "acct-42-run-1",
"cookies": [
{"name": "session", "value": "eyJhbGciOi...", "domain": "app.sparkproxy.io", "path": "/"}
]
}'
Or perform the login inside the API with a js_scenario, then reuse the same session_id to scrape while staying authenticated on one IP:
import requests
API = "https://scrape.sparkproxy.io/api/v1"
KEY = {"X-API-Key": "sk-your-api-key"}
# 1. Log in once. session_id pins the browser profile, its cookies, and the proxy IP.
requests.post(API, headers=KEY, json={
"url": "https://app.sparkproxy.io/login",
"render_js": True,
"session_id": "acct-42",
"premium_proxy": True, # route through a residential IP
"country_code": "US",
"js_scenario": {"steps": [ # actions follow the docs: fill, click, wait
{"fill": {"selector": "input[name=email]", "value": "you@sparkproxy.io"}},
{"fill": {"selector": "input[name=password]", "value": "your-password"}},
{"click": {"selector": "button[type=submit]"}},
{"wait_for": {"selector": "text=Dashboard"}}
]},
})
# 2. Reuse the SAME session_id: still logged in, same IP, cookies intact.
html = requests.post(API, headers=KEY, json={
"url": "https://app.sparkproxy.io/billing",
"render_js": True,
"session_id": "acct-42",
"premium_proxy": True,
"country_code": "US",
}).text
print(len(html), "bytes scraped while authenticated")
The trade-off is the usual one: you hand off cookie, browser, and IP management in exchange for a per-request cost and less low-level control. Our breakdown of a web scraping API versus self-managed proxies walks through when each wins. For login-heavy scraping where sessions keep dropping, the API usually pays for itself in debugging time saved.
Detect Expired Sessions and Re-Authenticate
Sessions expire. The server times them out, rotates the token, or invalidates the cookie after a security event. A resilient scraper assumes this will happen and checks every response instead of trusting that it's still logged in.
The tell is consistent: a 401 or 403 status, or a silent 302 redirect to /login that returns 200 with the login HTML. Detect it, re-authenticate, and retry once.
def is_authenticated(resp) -> bool:
"""Return False when the session has expired."""
if resp.status_code in (401, 403):
return False
if resp.url.rstrip("/").endswith("/login"): # redirected to login
return False
if "name=\"password\"" in resp.text: # login form served instead of content
return False
return True
def fetch(session, url, relogin):
resp = session.get(url, timeout=15)
if not is_authenticated(resp):
relogin(session) # refresh cookies, then retry once
resp = session.get(url, timeout=15)
return resp
Don't retry forever. If a re-login also fails, stop and alert, because repeated failed logins are the fastest way to get an account locked or flagged. Back off, log the failure, and move on.
Common Errors and Fixes
| Error / Symptom | Cause | Fix |
|---|---|---|
| POST returns `403` or "token mismatch" | Missing or stale CSRF token | GET the login page first, extract the token, submit it on the same `Session` |
| Logged in, then redirected to `/login` after a few requests | Rotating proxy changed the IP mid-session | Use a sticky session (or a fixed `session_id` on the Scraping API) |
| Login succeeds in browser but `requests` stays logged out | Auth token is in `localStorage`, not a cookie | Export with Playwright `storage_state`; send the token as an `Authorization` header |
| New `Session()` per request loses the login | Cookie jar discarded each call | Create one `Session`, authenticate once, reuse it everywhere |
| Form submits but nothing happens; page needs JS | SPA builds the form at runtime | Drive the login with Playwright instead of `requests` |
| Session drops when you change User-Agent | Server binds the cookie to the UA | Keep the exact same User-Agent for login and scraping |
| `401` only after ~15 minutes | Token or sticky session expired | Detect expiry, re-authenticate, retry once (see above) |
| Works locally, blocked from the server | Datacenter IP flagged on a login page | Route login and scraping through the same residential/sticky IP |
Frequently asked questions
FAQ
It depends entirely on authorization. Scraping your own account or one you're permitted to manage, within the site's Terms of Service, is generally fine. Accessing another person's account or data without permission can breach the ToS and, in some jurisdictions, computer-misuse laws. Check the ToS and prefer an official API before you scrape.
Use requests.Session for plain HTML forms. POST your credentials, let the session store the returned cookie, and reuse that same session object for every authenticated request. You only need a browser when the login form is built in JavaScript or protected by runtime fingerprinting, in which case Playwright handles the login and you can hand the cookies back to requests afterward.
Almost always because your IP changed. Many sites bind the session cookie to the IP (and sometimes the User-Agent) it was issued to, so a rotating proxy that hands you a new IP per request looks like a hijacked session and forces a re-login. Pin one IP with a sticky session for the whole login flow to fix it.
Often, yes. If a login POST returns 403 or a token error, the form requires a CSRF token. Fetch the login page first, read the token from its hidden input or meta tag, and submit it with your credentials on the same session. The token is tied to that session's cookie, so both steps must share one client.
Use a sticky proxy session, which holds one exit IP for a set window instead of rotating per request. With raw proxies you request the session through the username; with the SparkProxy Scraping API you pass a fixed session_id, and every call with that ID reuses the same IP and cookies.
Yes. Pass a js_scenario to fill and submit the login form, and a session_id to persist the resulting browser profile, cookies, and proxy IP. Reuse that same session_id on later requests and you scrape after login on one consistent, authenticated session. You can also inject existing cookies with the cookies parameter.
Get 50% off your first purchase
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Offer ends soon — claim it before it's gone
Related articles

How to Scrape Trustpilot Reviews with Proxies
Learn to scrape Trustpilot reviews with proxies: extract rating, text, date, reviewer, and company replies as JSON, paginate at scale, and stay GDPR-compliant.

How to Scrape eBay Listings and Prices
Learn how to scrape eBay listings and prices: extract title, price, bids, condition, and sold data, handle search pagination, and get past eBay anti-bot checks.

Java Web Scraping Proxy: Jsoup and HttpClient Guide
Java web scraping proxy guide: set Jsoup.connect proxies, fix the HTTPS auth gotcha, rotate IPs, parse pages with selectors, and call the SparkProxy API.
