Quick Start¶
Install¶
Note: We only support Python 3.10 and above. Python 3.9 has reached its end of life.
Via pip¶
The simplest way is to install from PyPI:
pip install curl_cffi --upgrade
We have sdist(source distribution) and bdist(binary distribution) on PyPI. This should work on Linux, macOS and Windows out of the box.
If it does not work on you platform, you may need to compile and install curl-impersonate
first and set some environment variables like LD_LIBRARY_PATH.
Beta versions¶
To install beta releases:
pip install curl_cffi --upgrade --pre
Note the --pre option here means pre-releases.
Latest¶
To install the latest unstable version from GitHub:
git clone https://github.com/lexiforest/curl_cffi/
cd curl_cffi
make preprocess
pip install .
Or you can download the wheels from github actions artifacts.
requests-like¶
curl_cffi tries to follow the requests API when possible, if you are already of guru using requests,
read the warning part in this page, skip other parts and head over to the Compatibility with requests.
Basic GET requests¶
Basic GET request and using the impersonate parameter.
import curl_cffi
url = "https://tls.browserleaks.com/json"
# Notice the impersonate parameter
r = curl_cffi.get("https://tls.browserleaks.com/json", impersonate="chrome110")
print(r.json())
# output: {..., "ja3n_hash": "aa56c057ad164ec4fdcb7a5a283be9fc", ...}
# the js3n fingerprint should be the same as target browser
# To keep using the latest browser version as `curl_cffi` updates,
# simply set impersonate="chrome" without specifying a version.
# Other similar values are: "safari" and "safari_ios"
r = curl_cffi.get("https://tls.browserleaks.com/json", impersonate="chrome")
# http/socks proxies are supported
proxies = {"https": "http://localhost:3128"}
r = curl_cffi.get("https://tls.browserleaks.com/json", impersonate="chrome110", proxies=proxies)
proxies = {"https": "socks://localhost:3128"}
r = curl_cffi.get("https://tls.browserleaks.com/json", impersonate="chrome110", proxies=proxies)
URL params¶
Messing with the URLs:
import curl_cffi
>>> params = {"foo": "bar"}
>>> r = requests.get("http://httpbin.org/get", params=params)
>>> r.url
'http://httpbin.org/get?foo=bar'
>>> params = {'key1': 'value1', 'key2': ['value2', 'value3']}
>>> import curl_cffi
>>> r = curl_cffi.get('https://httpbin.org/get', params=params)
>>> r.url
'https://httpbin.org/get?key1=value1&key2=value2&key2=value3'
Headers¶
Additional headers can be override with headers=....
headers = {"User-Agent": "curl_cffi/0.11.2"}
r = curl_cffi.get("http://example.com", headers=headers)
Warning
In curl_cffi, if you set impersonate=..., by default, the corresponding headers will
be added, you can:
Add your headers to override them.
Use
default_headers=Falseto completely turn off the default headers.Use
curl_cffi.get_fingerprint(...)and pass the result toimpersonate=...for fully editable custom fingerprints.
fingerprint = curl_cffi.get_fingerprint("edge_146_macos_26")
fingerprint.headers["User-Agent"] = "..."
r = curl_cffi.get(
"https://httpbin.org/headers",
impersonate=fingerprint,
)
>>> r = curl_cffi.get("https://httpbin.org/headers", impersonate="chrome")
>>> print(r.text)
{
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
"Accept-Encoding": "gzip, deflate, br, zstd",
"Accept-Language": "en-US,en;q=0.9",
"Host": "httpbin.org",
"Priority": "u=0, i",
"Sec-Ch-Ua": "\"Chromium\";v=\"136\", \"Google Chrome\";v=\"136\", \"Not.A/Brand\";v=\"99\"",
"Sec-Ch-Ua-Mobile": "?0",
"Sec-Ch-Ua-Platform": "\"macOS\"",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
"X-Amzn-Trace-Id": "Root=1-68452cc9-7287427f222e720c57971297"
}
}
>>> r = curl_cffi.get("https://httpbin.org/headers", impersonate="chrome", default_headers=False)
>>> print(r.text)
{
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate, br",
"Host": "httpbin.org",
"X-Amzn-Trace-Id": "Root=1-68452d20-2cf4cf00201987301c476c06"
}
}
Reading Response¶
Like requests, you can read the response content in the following ways:
Reading the binary content as bytes:
>>> r = curl_cffi.get("https://example.com")
>>> r.content
b'<!doctype html>\n<html>\n<head>\n...'
Reading the decoded content as str:
>>> r = curl_cffi.get("https://example.com")
>>> r.text
'<!doctype html>\n<html>\n<head>\n...'
By default, curl_cffi first use the encoding attribute if given, then tries to use the
Content-Type header to decode the content, If not found, will fallback to default_encoding,
then to “utf-8”.
# force override with .encoding
>>> r.encoding = 'latin-1'
POST and uploads¶
Of course, we also support POST, PUT, DELETE etc.
Form submit¶
Use the data={...} option.
Note
The “application/x-www-form-urlencoded” will be automatically added.
>>> r = curl_cffi.post("https://httpbin.org/post", data={"name": "Luke"})
>>> print(r.text)
{
"args": {},
"form": {
"name": "Luke"
},
...
}
Binary data¶
Still, use the data=b"..." option.
>>> r = curl_cffi.post("https://httpbin.org/post", data=b"LukeSkywalker")
>>> print(r.text)
{
"args": {},
"data": "LukeSkywalker",
"files": {},
"form": {},
...
}
Posting JSON¶
Use the json=... option.
Note
The “application/json” will be automatically added.
>>> r = curl_cffi.post("https://httpbin.org/post", json={"name": "Luke"})
>>> print(r.text)
{
"args": {},
"data": "{\"name\":\"Luke\"}",
"files": {},
"form": {},
...
}
Uploads¶
Warning
curl_cffi does not support the files=... API. Use multipart=... instead.
For uploading files, the requests API is horrible, we provide a similar but cleaner way:
mp = curl_cffi.CurlMime()
mp.addpart(
name="attachment", # field name in the form
content_type="image/png", # mime type
filename="image.png", # filename seen by remote server
local_path="./image.png", # local file to upload
data=file.read(), # if you already have the data in memory
)
r = curl_cffi.post("https://httpbin.org/post", data={"foo": "bar"}, multipart=mp)
print(r.json())
All the fields in the API are explicit. For advanced usage: see examples.
Compressed response¶
Currently, we forcefully decode compressed response, but this may be changed in the future.
curl_cffi supports gzip/brotli/zstd natively.
If the response content is json, you can parse them directly:
>>> r = curl_cffi.get("https://httpbin.org/headers")
>>> r.json()
{'headers': {'Accept': '*/*', 'Accept-Encoding': 'gz...')
Response status¶
>>> r = curl_cffi.get('https://httpbin.org/get')
>>> r.status_code
200
>>> r = curl_cffi.get("https://httpbin.org/status/404")
>>> r.status_code
404
>>> r.raise_for_status()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/.../repos/curl_cffi/curl_cffi/requests/models.py", line 167, in raise_for_status
raise HTTPError(f"HTTP Error {self.status_code}: {self.reason}", 0, self)
curl_cffi.requests.exceptions.HTTPError: HTTP Error 404:
Response headers¶
Response headers is a case-insensitive dict.
>>> r.headers
Headers({'date': 'Sun, 08 Jun 2025 06:58:15 GMT', 'content-type': 'application/json', 'content-length': '184', 'server': 'gunicorn/19.9.0', 'access-control-allow-origin': '*', 'access-control-allow-credentials': 'true'})
>>> r.headers["content-type"]
'application/json'
For a complete list of response attributes, see the API References.
Streaming response¶
For compatibility, curl_cffi supports the stream=True option, and a stream method,
with iterative-style content streaming.
But when possible, you should choose the native content_callback option.
r = curl_cffi.get(
>>> r = curl_cffi.get("https://httpbin.org/stream/20", stream=True)
>>> for chunk in r.iter_content():
... print("CHUNK", chunk)
...
CHUNK b'{"url": "https://httpbin.org/stream/20",...'
CHUNK b'{"url": "https://httpbin.org/stream/20",...'
For more examples, see the examples on GitHub
Warning
Natively, libcurl only support a callback-style API, i.e. you pass a callback function for processing the streamed response content. In curl_cffi, we use a internal queue to convert the callback to a interative API.
Because of the limitation, when a request is sent, the response will start to be streamed to your client at once. You need to start consuming the content immediately, otherwise, it would be store in your memory, thus OOM could be triggered.
Alternatively, you should use the native content_callback API.
>>> def callback(chunk):
... print("CHUNK", chunk)
...
>>> r = curl_cffi.get("https://httpbin.org/stream/20", content_callback=callback)
CHUNK b'{"url": "https://httpbin.org/stream/20"...'
CHUNK b'{"url": "https://httpbin.org/stream/20"...'
Redirection and history¶
curl_cffi automatically follows redirects, use allow_redirects=False to disable it.
>>> r = curl_cffi.get("https://httpbin.org/redirect-to?url=/")
>>> r.url
'https://httpbin.org/'
>>> r = requests.get("https://httpbin.org/redirect-to?url=/", allow_redirects=False)
>>> r.url
'https://httpbin.org/redirect-to?url=/'
>>> r.status_code
302
Warning
History is not implemented.
Authenticate¶
You can use the auth parameter or URL to add http basic auth credentials.
>>> curl_cffi.get("https://example.com", auth=("my_user", "password123"))
>>> curl_cffi.get("https://user:password@example.com")
Digest auth is dangerous and deprecated, we do not support that. Although, you should be able to use it with low-level curl options.
Asyncio¶
Besides the regular sync API, curl_cffi also provides a very similar asyncio API.
# You must use a session for asyncio
async with curl_cffi.AsyncSession() as s:
r = await s.get("https://example.com")
The benefit of asyncio is easier way to implement more concurrency:
import asyncio
from curl_cffi import AsyncSession
urls = [
"https://google.com/",
"https://facebook.com/",
"https://apple.com/",
]
async with AsyncSession() as s:
tasks = []
for url in urls:
task = s.get(url)
tasks.append(task)
results = await asyncio.gather(*tasks)
For detailed asyncio guide, see Asyncio.
WebSockets¶
curl_cffi supports both sync and async API for websockets.
from curl_cffi import Session, WebSocket
def on_message(ws: WebSocket, message):
print(message)
with Session() as session:
ws = session.ws_connect(
"wss://api.gemini.com/v1/marketdata/BTCUSD",
on_message=on_message,
)
ws.run_forever()
# asyncio
import asyncio
from curl_cffi import AsyncSession
async with AsyncSession() as session:
async with session.ws_connect("wss://echo.websocket.org") as ws:
await asyncio.gather(*[ws.send_str("Hello, World!") for _ in range(10)])
async for message in ws:
print(message)
For detailed websocket guide, see WebSockets.