PhantomClient 3.1.0

dotnet add package PhantomClient --version 3.1.0
                    
NuGet\Install-Package PhantomClient -Version 3.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PhantomClient" Version="3.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PhantomClient" Version="3.1.0" />
                    
Directory.Packages.props
<PackageReference Include="PhantomClient" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PhantomClient --version 3.1.0
                    
#r "nuget: PhantomClient, 3.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PhantomClient@3.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PhantomClient&version=3.1.0
                    
Install as a Cake Addin
#tool nuget:?package=PhantomClient&version=3.1.0
                    
Install as a Cake Tool

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 HttpClient gets 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\sidecar on Windows, ~/.cache/PhantomClient/sidecar on 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_103 through chrome_112, chrome_116_PSK, chrome_116_PSK_PQ, chrome_117, chrome_120, chrome_124, chrome_131 (default), chrome_131_PSK
  • Firefox: firefox_102 through firefox_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_7 through okhttp4_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 using or Dispose().
  • Use BrowserProfiles so 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • 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.

Version Downloads Last Updated
3.1.0 294 7/7/2026
3.0.0 136 7/7/2026
2.0.3 208 6/16/2026
2.0.1 127 5/25/2026
1.0.9 231 10/11/2025
1.0.8 163 10/11/2025
1.0.7 167 10/11/2025
1.0.5 157 10/11/2025
1.0.4 153 10/10/2025
1.0.3 154 10/10/2025
1.0.2 145 10/10/2025
1.0.1 154 10/10/2025