PhantomClient 2.0.1

dotnet add package PhantomClient --version 2.0.1
                    
NuGet\Install-Package PhantomClient -Version 2.0.1
                    
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="2.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PhantomClient" Version="2.0.1" />
                    
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 2.0.1
                    
#r "nuget: PhantomClient, 2.0.1"
                    
#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@2.0.1
                    
#: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=2.0.1
                    
Install as a Cake Addin
#tool nuget:?package=PhantomClient&version=2.0.1
                    
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.

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
2.0.1 93 5/25/2026
1.0.9 215 10/11/2025
1.0.8 148 10/11/2025
1.0.7 154 10/11/2025
1.0.5 145 10/11/2025
1.0.4 141 10/10/2025
1.0.3 141 10/10/2025
1.0.2 135 10/10/2025
1.0.1 140 10/10/2025