PhantomClient 2.0.1
dotnet add package PhantomClient --version 2.0.1
NuGet\Install-Package PhantomClient -Version 2.0.1
<PackageReference Include="PhantomClient" Version="2.0.1" />
<PackageVersion Include="PhantomClient" Version="2.0.1" />
<PackageReference Include="PhantomClient" />
paket add PhantomClient --version 2.0.1
#r "nuget: PhantomClient, 2.0.1"
#:package PhantomClient@2.0.1
#addin nuget:?package=PhantomClient&version=2.0.1
#tool nuget:?package=PhantomClient&version=2.0.1
PhantomClient
A .NET HTTP client that makes your requests look like they come from a real browser. It copies real browser TLS fingerprints, so you can reach sites that sit behind Cloudflare, DataDome and similar anti-bot services.
What it does
PhantomClient sends HTTP requests whose TLS handshake matches a real browser instead of a generic .NET client. That makes it useful for:
- Scraping sites that block normal HTTP clients
- Testing APIs that have anti-bot protection in front of them
- Automating sites protected by Cloudflare or DataDome
- Any case where a plain
HttpClientgets a 403 but a browser works
Features
- Multiple browser fingerprint profiles (Chrome, Firefox, Safari, and more)
- All HTTP methods: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
- Automatic cookie handling per client instance
- Proxy support (HTTP and SOCKS5)
- Header order control
- JSON and form-encoded bodies
- Configurable timeout
- Simple async API
Installation
dotnet add package PhantomClient
Or from the Package Manager Console:
Install-Package PhantomClient
Requires .NET 9.0 or later. There is no setup step; create a client and start making requests.
Quick start
using PhantomClientCore;
using var client = new PhantomClient();
var response = await client.GetAsync("https://example.com");
Console.WriteLine($"Status: {response.Status}");
Console.WriteLine($"Body: {response.Body}");
Usage
GET with custom headers
using PhantomClientCore;
using var client = new PhantomClient(new PhantomClientOptions
{
ClientIdentifier = "chrome_131",
Timeout = 30000
});
var response = await client.GetAsync("https://api.example.com/data", new RequestOptions
{
Headers = new Dictionary<string, string>
{
["Accept"] = "application/json",
["Accept-Language"] = "en-US,en;q=0.9"
}
});
if (response.IsSuccess)
Console.WriteLine(response.Body);
POST with a JSON body
using PhantomClientCore;
using System.Text.Json;
using var client = new PhantomClient();
var jsonBody = JsonSerializer.Serialize(new { username = "testuser", password = "secret123" });
var response = await client.PostAsync("https://api.example.com/login", new PostRequestOptions
{
Body = jsonBody,
Headers = new Dictionary<string, string>
{
["Content-Type"] = "application/json",
["Accept"] = "application/json"
}
});
Console.WriteLine(response.Body);
POST with form data
using PhantomClientCore;
using var client = new PhantomClient();
var formData = new Dictionary<string, string>
{
["username"] = "testuser",
["password"] = "secret123",
["remember"] = "true"
};
var response = await client.PostFormAsync("https://example.com/login", formData);
if (response.IsSuccess)
Console.WriteLine("Login successful");
Getting past Cloudflare or DataDome
A browser TLS fingerprint on its own is usually not enough. These services also look at which headers you send, the order they arrive in, and whether your TLS handshake varies the way a real browser's does. BrowserProfiles sets all of that up for you: it picks a fingerprint and sends the matching browser headers in the right order.
using PhantomClientCore;
// Chrome fingerprint plus matching headers, header order, and randomized TLS extension order.
using var client = new PhantomClient(BrowserProfiles.Chrome131());
// 1. Load a normal page first so the site can hand this client its cookie.
await client.GetAsync("https://protected-site.com/", new RequestOptions
{
Headers = BrowserProfiles.NavigationHeaders()
});
// 2. Call the API as a same-site XHR. Add any site-specific headers you need.
var apiHeaders = BrowserProfiles.XhrHeaders(
origin: "https://protected-site.com",
referer: "https://protected-site.com/search");
apiHeaders["content-type"] = "application/json";
var response = await client.PostAsync("https://api.protected-site.com/search",
new PostRequestOptions { Headers = apiHeaders, Body = "{\"q\":\"phone\"}" });
Console.WriteLine(response.IsSuccess ? "OK" : $"Blocked: {response.Status}");
Why the first request? Anti-bot cookies are tied to the client that received them. A cookie copied out of a real browser will not work here because the fingerprint is different. The reliable approach is to let this client earn its own cookie by loading a normal page first, then call the API on the same instance.
Sessions and cookies
using PhantomClientCore;
using var client = new PhantomClient();
// Cookies set on one request are reused on the next, automatically.
await client.PostFormAsync("https://example.com/login", new Dictionary<string, string>
{
["username"] = "user",
["password"] = "pass"
});
var profile = await client.GetAsync("https://example.com/profile");
Console.WriteLine(profile.Body);
// Inspect or clear stored cookies for a domain.
var cookies = client.Cookies.GetCookies("https://example.com");
client.Cookies.ClearCookies("https://example.com");
Using a proxy
using PhantomClientCore;
using var client = new PhantomClient(new PhantomClientOptions
{
Proxy = "http://username:password@proxy.example.com:8080"
});
var response = await client.GetAsync("https://api.ipify.org?format=json");
// You can also override the proxy for a single request.
var response2 = await client.GetAsync("https://example.com", new RequestOptions
{
Proxy = "http://another-proxy.com:8080"
});
Isolated client instances
Each client keeps its own cookies and session.
using PhantomClientCore;
using var client1 = new PhantomClient(new PhantomClientOptions { ClientIdentifier = "chrome_131" });
using var client2 = new PhantomClient(new PhantomClientOptions { ClientIdentifier = "firefox_133" });
await client1.GetAsync("https://httpbin.org/cookies/set?session=abc123");
// client2 has its own cookie container and does not see client1's cookies.
var c1 = client1.Cookies.GetCookies("https://httpbin.org");
var c2 = client2.Cookies.GetCookies("https://httpbin.org");
Redirects
using PhantomClientCore;
using var client = new PhantomClient();
// Followed by default.
var followed = await client.GetAsync("https://httpbin.org/redirect/2");
Console.WriteLine(followed.Url); // final URL
// Or stop on the first response.
var notFollowed = await client.GetAsync("https://httpbin.org/redirect/1", new RequestOptions
{
FollowRedirect = false
});
Console.WriteLine(notFollowed.Status); // 3xx
Retry with backoff
using PhantomClientCore;
using var client = new PhantomClient();
async Task<TlsResponse?> GetWithRetry(string url, int maxRetries = 3)
{
for (int i = 0; i < maxRetries; i++)
{
try
{
var response = await client.GetAsync(url);
if (response.IsSuccess) return response;
}
catch (Exception ex)
{
Console.WriteLine($"Attempt {i + 1}: {ex.Message}");
}
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)));
}
return null;
}
var result = await GetWithRetry("https://example.com");
API reference
PhantomClient
public PhantomClient(PhantomClientOptions? options = null)
Creates a client. Each instance has its own session and cookies. Implements IDisposable and IAsyncDisposable.
| Member | Description |
|---|---|
CookieContainer Cookies |
Cookies seen on responses, reused on later requests |
GetAsync(url, options?) |
GET request |
PostAsync(url, options?) |
POST with a JSON or raw body |
PostFormAsync(url, formData, options?) |
POST as application/x-www-form-urlencoded |
PutAsync / PatchAsync |
PUT / PATCH with a body |
DeleteAsync / HeadAsync / OptionsAsync |
DELETE / HEAD / OPTIONS |
PhantomClientOptions
public class PhantomClientOptions
{
public string ClientIdentifier { get; set; } = "chrome_131"; // browser fingerprint
public int Timeout { get; set; } = 30000; // milliseconds
public string? Proxy { get; set; }
public Dictionary<string, string>? DefaultHeaders { get; set; } // merged into every request
public List<string>? HeaderOrder { get; set; } // HTTP/2 header order (lower-case)
public bool RandomTlsExtensionOrder { get; set; } = true; // vary the handshake like real Chrome
public bool InsecureSkipVerify { get; set; } = false;
public bool ForceHttp1 { get; set; } = false; // false uses HTTP/2 from the profile
}
BrowserProfiles
PhantomClientOptions BrowserProfiles.Chrome131(string? proxy = null); // recommended preset
Dictionary<string,string> BrowserProfiles.NavigationHeaders(); // headers for loading a page
Dictionary<string,string> BrowserProfiles.XhrHeaders(string origin, string referer); // headers for an API call
RequestOptions / PostRequestOptions
public class RequestOptions
{
public Dictionary<string, string>? Headers { get; set; }
public Dictionary<string, string>? Cookies { get; set; }
public List<string>? HeaderOrder { get; set; } // overrides the session header order
public bool FollowRedirect { get; set; } = true;
public string? Proxy { get; set; }
}
public class PostRequestOptions : RequestOptions
{
public string? Body { get; set; }
public Dictionary<string, string>? FormData { get; set; }
}
TlsResponse
public class TlsResponse
{
public int Status { get; set; }
public string StatusText { get; set; }
public Dictionary<string, string> Headers { get; set; }
public string Body { get; set; }
public Dictionary<string, string> Cookies { get; set; }
public string Url { get; set; } // final URL after redirects
public bool IsSuccess { get; } // status in 200..299
}
Browser fingerprints
Set ClientIdentifier to one of the supported profiles:
- Chrome:
chrome_103throughchrome_112,chrome_116_PSK,chrome_116_PSK_PQ,chrome_117,chrome_120,chrome_124,chrome_131(default),chrome_131_PSK - Firefox:
firefox_102throughfirefox_133 - Safari:
safari_15_6_1,safari_16_0,safari_ipad_15_6,safari_ios_15_5,safari_ios_16_0,safari_ios_17_0,safari_ios_18_0 - Other:
opera_89/opera_90/opera_91,okhttp4_android_7throughokhttp4_android_13,nike_ios_mobile,mesh_ios,mesh_android,confirmed_ios,cloudscraper
For anti-bot targets, use a recent profile and pair it with BrowserProfiles.Chrome131() so the headers match the fingerprint. Old profiles are easier to flag.
Tips
- Reuse one client for a logical session instead of creating one per request.
- Dispose clients with
usingorDispose(). - Use
BrowserProfilesso your headers match the fingerprint you picked. - Space out requests so you do not trip rate limits.
- Each client has its own cookies, so use separate instances for isolated sessions.
Troubleshooting
Getting a 403 or a captcha:
- Use a recent profile through
BrowserProfiles.Chrome131(). - Load a normal page first so the site can set its cookies, then call the API on the same client.
- Slow down, and try a residential proxy.
Timeouts:
- Raise
Timeout, and check your network and proxy settings.
Cookie confusion:
- Each client has its own cookie container. Inspect it with
client.Cookies.GetCookies(url).
License
MIT License. Copyright (c) 2025 Riadh Chebbi.
Disclaimer
This library is meant for legitimate uses such as testing, research, and automating your own services. Respect each site's Terms of Service and robots.txt. The authors are not responsible for misuse.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.