Verify an impersonation library
Libraries like curl-impersonate, curl_cffi, tls-client and rnet all promise to make your requests indistinguishable from a named browser. Whether the build you actually installed still does that — after your package manager resolved it, after a transitive dependency moved, after the browser it targets shipped a new version — is an empirical question with a one-command answer.
The check
Point the client at /api/clean and read what came back. The response describes the connection it arrived on, so this measures the client you used, not the machine you ran it from.
$ curl_chrome116 -s https://kittens.sh/api/clean
{
"ja3_hash": "cd08e31494f9531f560d64c695473da9",
"ja4": "t13d1516h2_8daaf6152771_b0da82dd1658",
"akamai_hash": "52d84b11737d980aef856699f885ca86",
"user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) ..."
}Getting the target to compare against
A fingerprint on its own tells you nothing — you need the one you are trying to match. Open the inspector in the real browser, on the real platform, and copy its JA4. Two things make this go wrong:
- Platform matters. Chrome on macOS and Chrome on Windows do not produce identical handshakes. Impersonation targets are usually specified as browser-plus-platform; compare against the same one.
- Browser JA3 is not a stable target. Chrome randomises extension order, so its JA3 differs on every reload. Compare JA4, which sorts before hashing. If a tool advertises a fixed Chrome JA3, that claim expired in 2023.
Compare all three layers
A matching JA4 is necessary and not sufficient. The three fingerprints come from different layers and are defeated independently:
| Match | Mismatch means |
|---|---|
| ja4 | The TLS handshake itself is wrong. Usually the build is not being used at all — see below. |
| akamai_hash | TLS is right and HTTP/2 is not: wrong SETTINGS, no WINDOW_UPDATE, or the pseudo-headers in the wrong order. Common in libraries that patched the TLS layer and left the HTTP layer stock. |
| user_agent | The easy one, and the one people get wrong anyway by setting a Chrome UA on a build that impersonates Firefox. |
/api/http2 returns the frames unhashed, so when the Akamai hash differs you can see which setting moved rather than guessing.
When the JA4 is completely wrong
If the fingerprint looks nothing like a browser, the impersonating build is almost certainly not the one that ran. In rough order of how often this is the cause:
- The system curl answered. curl-impersonate ships wrapper scripts —
curl_chrome116and friends — that set the flags and preload the patched library. Invoking plaincurlgets you your distribution’s build with your distribution’s OpenSSL. Check withwhichand confirm the fingerprint changes between the two. - The wrapper found the wrong library. The wrappers rely on
LD_PRELOAD(orDYLD_INSERT_LIBRARIESon macOS). If the patched libcurl is not where the script expects, the preload silently fails and the request still succeeds — with the stock stack. - A proxy is terminating TLS. Corporate interception, mitmproxy, or a debugging proxy will complete the handshake itself and open a fresh one to the origin. What gets measured is the proxy. This is worth ruling out first if the fingerprint is stable but unfamiliar.
- In Python, the wrong session type.
curl_cffionly impersonates when you ask it to. A barerequests.getfrom the standard library is still the standard library.
# curl_cffi: impersonation is opt-in, per request or per session
from curl_cffi import requests
r = requests.get("https://kittens.sh/api/clean", impersonate="chrome")
print(r.json()["ja4"])Then keep it verified
Fingerprints drift silently — a rebuilt image, a bumped dependency, a new upstream release — and the failure mode is a client that quietly stops matching its target weeks before anyone notices. Pinning the fingerprint in CI turns that into a failed build. If the User-Agent and the handshake are the pair that disagree, that case has its own page.