PhantomClient 3.1.0
dotnet add package PhantomClient --version 3.1.0
NuGet\Install-Package PhantomClient -Version 3.1.0
<PackageReference Include="PhantomClient" Version="3.1.0" />
<PackageVersion Include="PhantomClient" Version="3.1.0" />
<PackageReference Include="PhantomClient" />
paket add PhantomClient --version 3.1.0
#r "nuget: PhantomClient, 3.1.0"
#:package PhantomClient@3.1.0
#addin nuget:?package=PhantomClient&version=3.1.0
#tool nuget:?package=PhantomClient&version=3.1.0
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. On the first request the library downloads its engine (see How it works), so the very first call needs network access and takes a little longer.
How it works
The browser TLS fingerprints come from the Go bogdanfinn/tls-client engine. PhantomClient runs that engine as a local sidecar: a small phantom-sidecar process that listens on 127.0.0.1, and the library forwards each request to it over HTTP (POST /api/forward) and turns the reply into a TlsResponse.
The sidecar runs out-of-process on purpose. Hosting the Go runtime inside the .NET process makes Go's garbage collector and async-preemption signal handlers fight the CLR's on Linux, which can take the whole host down with an uncatchable crash under concurrent load. Running the engine as a separate process keeps the two runtimes in separate address spaces, so the host stays stable.
On the first request the library downloads the phantom-sidecar binary for your OS and architecture and caches it, then launches it as a managed child process:
- The binary is pinned to a known version and downloaded once to a per-user cache:
%LOCALAPPDATA%\PhantomClient\sidecaron Windows,~/.cache/PhantomClient/sidecaron Linux and macOS. - It is about 12 MB (a thin loopback shell over the tls-client engine, no framework bloat), so it is downloaded on first use rather than shipped inside the NuGet package.
- The child is launched directly with a random free loopback port and a random per-launch API key passed as command-line flags (no config file). The library confirms it is serving, restarts it if it dies, and kills it when the host process exits.
- Prebuilt binaries exist for Windows amd64, Linux amd64, and macOS amd64 and arm64 (Windows on ARM runs the amd64 build under emulation). There is no prebuilt Linux arm64 or 32-bit Windows binary. It is a static Go binary, so on Linux it needs no extra OS packages.
You normally do not have to think about any of this. If you want to preload or prefetch the binary at deploy time, call PhantomTLS.InitializeAsync(); PhantomTLS.DestroyAsync() releases the sidecar sessions.
Configuration via environment variables
| Variable | Effect |
|---|---|
PHANTOM_SIDECAR_URL |
Use an externally managed sidecar at this URL instead of auto-launching one. |
PHANTOM_SIDECAR_KEY |
API key for the external sidecar named by PHANTOM_SIDECAR_URL. |
PHANTOM_SIDECAR_BIN |
Path to a phantom-sidecar binary to use instead of downloading one. |
PHANTOM_SIDECAR_VERSION |
Pin a specific phantom-sidecar version to download. |
PHANTOM_SIDECAR_DEBUG |
Set to 1 to surface the sidecar's own logs. |
Security note
The sidecar binds 127.0.0.1 only, so it is not reachable from the network and never triggers a firewall prompt. The random per-launch API key (X-API-KEY) is defence against other processes on the same machine. To run your own sidecar instead (for example in a container), point the library at it with PHANTOM_SIDECAR_URL.
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.