Guide

How to Use curl-impersonate (and curl_cffi) With Proxies

Combine a real browser TLS and HTTP2 fingerprint with a proxy IP to pass anti-bot: curl_cffi impersonate plus proxies in Python, the curl-impersonate binary with -x, and verifying both your JA3 and your exit IP.

HProxy Team · ·Updated July 17, 2026 ·7 min read
HProxy. Guide

Free proxies won't hold up here.

Shared datacenter IPs get flagged and dropped fast. When it has to hold, gaming, streaming, accounts, you need mobile and residential IPs that read as a real device, from $0.65/GB, pay as you go.

See plans & pricing

Here is the thing nobody tells you when your residential proxy still gets blocked: the IP was never the only signal. A modern anti-bot system reads the shape of your TLS handshake and your HTTP/2 settings before it has looked at a single byte of your request, and a raw Python client announces itself as a script in that handshake no matter how clean the IP behind it is. curl-impersonate exists to fix exactly that layer. This guide is about using it the way it is actually useful: a real browser's fingerprint and a real proxy's IP, together, because each one alone leaves the other signal screaming "bot."

We run a proxy network and spend a lot of time on the defender's side of this, watching which requests get flagged and why. The pattern is consistent: people blame the proxy for a block that the TLS fingerprint caused. Every example below is copy-paste runnable. Where one needs a live proxy, pull a fresh one from our free proxy API; for anything facing a real anti-bot wall you will want residential IPs, for reasons this guide makes concrete.

How do you use curl-impersonate with a proxy?

In Python, use curl_cffi, the practical binding: requests.get(url, impersonate="chrome", proxies={"https": "http://user:pass@host:port"}) sets the browser fingerprint and the proxy in one call. From the shell, the curl-impersonate binary ships per-browser wrapper scripts that take -x for the proxy exactly like normal curl. The fingerprint and the proxy are set side by side, because you need both.

Why a proxy alone is not enough

When your client opens a TLS connection, the very first message (the ClientHello) lists the cipher suites, extensions, elliptic curves, and their exact order that your client supports. That combination is stable enough per client to hash into a fingerprint, the well-known JA3 and its successor JA4, and a real Chrome produces a very different hash from Python's requests or httpx. HTTP/2 adds a second fingerprint from the order and values of its settings frames. An anti-bot service compares your handshake against the fingerprint of the browser your User-Agent claims to be, and when a request says "I am Chrome" in the header but hands over Python's TLS fingerprint, that contradiction is one of the cheapest, most reliable bot signals there is.

No proxy touches this. The proxy changes the IP the target sees; the fingerprint travels end to end inside the TLS handshake, which the proxy only tunnels. So a flawless residential IP paired with a Python fingerprint gets you the worst of both: a believable address wearing an unmistakable script handshake. curl-impersonate closes the gap by patching curl's TLS library (and HTTP/2 layer) to reproduce a specific browser's handshake byte for byte. We wrote the full detection breakdown in how websites detect proxies; this is the tool that answers the fingerprint half of it.

curl_cffi: the Python way

For Python, curl_cffi is the tool. It binds curl-impersonate and exposes a requests-style API, so the only new thing to learn is the impersonate argument. Install it:

pip install curl_cffi

Then set the fingerprint and the proxy in the same call. curl_cffi takes the same proxies dict shape as the requests library:

from curl_cffi import requests

proxies = {
    "http": "http://user:[email protected]:8080",
    "https": "http://user:[email protected]:8080",
}

r = requests.get(
    "https://tls.browserleaks.com/json",   # echoes the JA3 the server saw
    impersonate="chrome",
    proxies=proxies,
)
print(r.json())

impersonate="chrome" selects the latest supported Chrome build; pass a pinned string like impersonate="chrome124" to lock a specific version. Per curl_cffi's docs the current release ships dozens of preset fingerprints, and curl-cffi list prints every target your installed version supports. It reproduces the JA3/TLS and HTTP/2 fingerprints, which is the pair that matters.

Reuse the fingerprint and proxy across requests with a Session, which also persists cookies through a login flow:

from curl_cffi import requests

session = requests.Session(impersonate="chrome", proxies=proxies)
session.get("https://example.com/login")
r = session.get("https://example.com/dashboard")  # same fingerprint, same proxy, cookies kept

For concurrency, curl_cffi ships an async client that takes the same two arguments:

import asyncio
from curl_cffi import AsyncSession

async def main():
    async with AsyncSession(impersonate="chrome", proxies=proxies) as s:
        r = await s.get("https://tls.browserleaks.com/json")
        print(r.json())

asyncio.run(main())

The curl-impersonate binary: the shell way

If you would rather stay on the command line, the curl-impersonate binary is a drop-in curl that already sends a browser's handshake. It ships a wrapper script per browser (named for the browser and version, for example curl_chrome123 or curl_ff133) that launches the binary with all the right headers and flags. Because it is curl underneath, the proxy flag is the same -x you already know:

# Browser fingerprint from the wrapper, proxy from -x, exactly like normal curl
curl_chrome123 -x http://user:[email protected]:8080 https://tls.browserleaks.com/json

Everything from our cURL proxy guide still applies here: -x takes the proxy URL, credentials go in -U user:pass or the URL userinfo, and a socks5h:// scheme routes over SOCKS with remote DNS. The wrappers cover recent Chrome, Firefox, Safari, Edge, and Tor builds. The binary is the same engine curl_cffi wraps, so pick whichever interface fits: the CLI for quick checks and shell pipelines, curl_cffi for anything programmatic.

Authentication and SOCKS

Because both interfaces are curl underneath, proxy authentication and SOCKS work exactly as they do in plain curl and requests. Put credentials in the proxy URL userinfo, http://user:pass@host:port, and if the password has URL-special characters, percent-encode it first. For SOCKS5, use a socks5h:// URL so the proxy does the DNS lookup instead of your machine leaking every hostname to its local resolver:

from curl_cffi import requests

proxies = {"https": "socks5h://user:[email protected]:1080"}
r = requests.get("https://httpbin.org/ip", impersonate="chrome", proxies=proxies)
print(r.json())

A wrong or missing password still comes back as HTTP 407 Proxy Authentication Required, which means the proxy is alive and only the credentials are the problem. The focused fix is in how to fix 407 Proxy Authentication Required.

Verify both signals

With ordinary proxies you verify one thing, the exit IP. With impersonation you verify two, because either can silently fail: the IP the target sees, and the fingerprint it reads. Check both against an endpoint that echoes them back:

from curl_cffi import requests

proxies = {"https": "http://203.0.113.7:8080"}

# 1) Did the proxy change the IP?
ip = requests.get("https://httpbin.org/ip", impersonate="chrome", proxies=proxies).json()
print("exit IP:", ip["origin"])

# 2) Does the TLS fingerprint look like a browser, not Python?
tls = requests.get("https://tls.browserleaks.com/json", impersonate="chrome", proxies=proxies).json()
print("JA3 hash:", tls.get("ja3_hash"))
print("user agent seen:", tls.get("user_agent"))

Run the same JA3 check with plain requests once, for contrast, and you will see a completely different hash: that is the fingerprint anti-bot systems flag. When curl_cffi is working, the JA3 matches the real browser you selected, and the exit IP is the proxy's. Our proxy checker covers the IP side, exit address, anonymity grade, real geolocation, and latency, in one paste.

The honest limits

Impersonation is the mirror image of what a proxy does, and it has the same kind of honest boundary. A proxy fixes the IP, not the fingerprint. curl_cffi fixes the fingerprint, not the JavaScript. It reproduces the TLS and HTTP/2 handshake, but it is not a browser engine and does not run a single line of JavaScript. So a Cloudflare managed challenge, a DataDome interstitial, or any defense that hands the client a script to execute will not be solved by curl_cffi alone. What it does solve is the passive fingerprint layer that sits in front of almost everything, which is why it sails through many JSON APIs and lightly defended pages that block raw requests outright.

Two more things keep it working. Keep curl_cffi updated, because browsers rotate their TLS every few weeks and a stale fingerprint eventually looks like an outdated browser, which is its own flag. And when the target does throw a JavaScript challenge, that is the point to move to a real browser, which we cover in how to scrape past Cloudflare. The realistic stack for a defended site is a real browser fingerprint (curl_cffi or an actual browser) on a real residential IP, with human-like pacing on top. None of the three is optional, and none of the three is a magic cloak.

Where to go from here

The mental model to keep is that detection happens in layers, and you clear them from the outside in: the IP (a clean residential proxy), the fingerprint (curl-impersonate or curl_cffi), then behavior and any JavaScript challenge (a real browser and sane pacing). This guide is the middle layer. Get it and the IP right together and you eliminate the two most common silent blocks in one move.

From here, how websites detect proxies is the full defender's-eye map of every signal, proxies for web scraping covers choosing the right proxy type for the target, and the cURL guide is the reference for every proxy flag the impersonate binary inherits. When a project graduates to production against real defenses, get IPs nobody else is burning on our paid pools.

Sources

  • curl_cffi (lexiforest/curl_cffi): the Python binding, its impersonate argument, the requests-style proxies support, Session/AsyncSession, and the JA3/TLS plus HTTP2 fingerprint impersonation it provides.
  • curl-impersonate (lexiforest/curl-impersonate): the patched curl binary, the per-browser wrapper scripts, the -x proxy flag inherited from curl, and the supported Chrome, Firefox, Safari, Edge, and Tor versions.
  • How websites detect proxies: our breakdown of the JA3/JA4 TLS and HTTP/2 fingerprints that a proxy cannot change.

Frequently asked questions

Does a proxy alone get me past Cloudflare or DataDome?
Often not. A residential proxy fixes your IP reputation, but modern anti-bot systems also read your TLS/JA3 and HTTP2 fingerprint, and a raw Python client like requests or httpx has an obviously non-browser fingerprint that no proxy can hide. curl-impersonate, and its Python binding curl_cffi, make that handshake identical to a real browser's. You need the clean IP and the browser fingerprint together, not either one alone.
What is the difference between curl-impersonate and curl_cffi?
curl-impersonate is a patched build of the curl C library that reproduces real browsers' TLS and HTTP2 handshakes, driven from the shell through per-browser wrapper scripts. curl_cffi is the Python binding to that same library, giving you a requests-style API with an extra impersonate argument. Same engine underneath, one is a command-line tool and one is a Python package.
How do I use a proxy with curl_cffi?
Pass a requests-style proxies dict, or the singular proxy argument, alongside impersonate: requests.get(url, impersonate='chrome', proxies={'https': 'http://user:pass@host:port'}). It is the same proxy syntax as the requests library, so existing proxy code ports across directly, and the impersonation and the proxy are set in the same call.
Does curl_cffi run JavaScript?
No. It impersonates the network-level fingerprint, the TLS and HTTP2 handshake, not a browser engine. So JavaScript challenges like Cloudflare's interstitial or DataDome's puzzle page are not solved by it. curl_cffi gets you past the passive fingerprint layer, which covers many JSON APIs and lightly defended pages, but for a real JavaScript challenge you still need an actual browser.
Which browsers can curl-impersonate mimic?
Recent Chrome, Firefox, Safari including iOS Safari, Edge, and Tor, each pinned to specific versions. In curl_cffi you pick one with impersonate='chrome' for the latest supported build of that browser, or a pinned string like impersonate='chrome124'. Run curl-cffi list to see every target your installed version supports, since the set grows with each release.

Proxies that don't die mid-job

Residential, ISP, datacenter and mobile, verified by the same engine that runs tens of millions of checks. They read as a real device and hold up under load. Pay as you go, and your balance never expires.

47M+ proxy checks run · 100+ countries · HTTP / HTTPS / SOCKS · re-checked every few minutes · no signup