TLS & HTTP/2 Fingerprinting
The Fingerprint You Never See
When your requests script gets blocked even though the headers are perfect and the IP is clean, the problem is not your IP and not your headers. It is the very first bytes of the handshake, the messages your transport layer sends before any HTTP exists. Modern anti-bot platforms read the shape of the TLS ClientHello and the HTTP/2 connection preface, hash it, and compare it against "what browsers look like." Every HTTP library has a distinctive, machine-like shape, and servers cluster you instantly.
This is why the phrases JA3 and JA4 show up in bot-detection literature. They are fingerprints of the ClientHello: cipher suites, the elliptic curves you offer, the extension list and its order, signature algorithms, and TLS version. Browsers ship with a large, ordered, intentionally noisy set of ciphers. requests, httpx, and curl each present a sparse, rigidly ordered set. Even if you send Google's exact User-Agent, the handshake has already announced you as a script before a single header arrives. HTTP/2 adds a second fingerprint at the next layer: the SETTINGS frame values and the order of your request headers.
Your Stack Tells on You
Try this experiment with any public bot-scoring service. First, requests with a copied Chrome User-Agent: high bot score. Then the same URL through your own browser: clean. You changed your head while forgetting to change your body. The header tells the server who the library pretends to be; the handshake tells it who the library actually is. Scoring engines weight the handshake heavily because it is far harder for a casual script to change.
The practical consequence: on strict sites, there is no header-only fix. You must either use a transport stack that produces a browser-shaped handshake, or pay the cost of a real browser.
Impersonating With curl_cffi
The pragmatic middle path in Python is curl_cffi: a build of libcurl linked against BoringSSL (Google's TLS implementation) with careful settings copied from real Chrome release handshakes. You request a browser impersonation and the library sends the right ciphers, the right extension order, and the right HTTP/2 settings.
from curl_cffi import requests as cr
resp = cr.get(
"https://example.com/api/items",
headers={"Accept": "application/json"},
impersonate="chrome124",
)
print(resp.status_code, resp.json()["count"])
impersonate values mirror Chrome and Firefox releases. Keep the impersonation version aligned with the era of site you target: a too-new Chrome against a legacy stack is as suspicious as an old one. curl_cffi also exposes a session with cookie and proxy handling that works like the httpx sessions from the earlier lessons, so swapping it into existing code is small.
Consistency Beats Variety
The deadliest detail beginners get wrong is mixing layers: a Chrome User-Agent on a curl handshake, an Accept-Language nobody else sends, or periodic random header mutations meant to "confuse" detection. Fingerprinting libraries score the whole surface. A script that changes its fingerprint on every request looks like malware evasion, not like traffic that should be ignored. Pick one complete persona (handshake + headers + HTTP/2 order + TLS settings), use it uniformly, and change it only when that persona stops working.
Realistic Expectations
Impersonation moves you past the cheap detection layers. On top sits a genuinely hostile site that pair the TLS score with JavaScript proof-of-work, behavioral scoring, and CAPTCHAs. That is not a header problem anymore, it is an anti-bot-system problem, and the resolution is either the internal-API strategy from earlier lessons or a real browser session that passes the challenges for you. Financial audits of public datasets usually want you to name the day you stop improving the fingerprint and start renting the infrastructure or driving the browser.