Automating Antidetect Profiles From C# With PuppeteerSharp and the REST API
Last month I helped a .NET team troubleshoot why their PuppeteerSharp scraping jobs kept hitting Cloudflare challenges after running fine for six hours. They'd done everything the forums recommend: randomized user agents, residential proxies, human-like delays. Still blocked.
I asked them to log navigator.webdriver from within the page context.
It returned true.
Ouch.
The thing is, PuppeteerSharp (like Puppeteer itself) connects to Chrome via the DevTools Protocol. That's its whole architecture. And Chrome, when controlled via CDP, sets navigator.webdriver = true by design. Every bot-detection service on the planet checks that flag. Patching it from JavaScript after page load? Detection services catch the override through getter traps and timing analysis. I spent a weekend learning this the hard way back in 2024 — basically threw hours at Stack Overflow solutions that all boiled down to "inject this script before page load" and none of them worked against anything serious. Embarrassing in hindsight.
The fix isn't patching. It's running PuppeteerSharp against a browser that never sets the flag in the first place.
What We're Building
By the end of this, you'll have:
- A C# method that launches JustBrowser profiles via REST API
- PuppeteerSharp connected to those profiles over CDP WebSocket
- Your existing automation code running against a browser with 40+ genuine fingerprint parameters
- Profile lifecycle management — start, connect, disconnect, stop
Your PuppeteerSharp code stays the same. You're just changing what browser it talks to. The new browser happens to have real fingerprint protection compiled into the Chromium source code.
Prerequisites
- .NET 6+ (examples use .NET 8, but 6/7 work fine)
- PuppeteerSharp 19+ via NuGet
- JustBrowser ($9.99/mo) — the REST API is live from day one of the 7-day trial
System.Net.Http.Jsonfor JSON handling (included in .NET 6+)- Basic familiarity with async/await in C#
If you're coming from Python or Node.js and wondering why this guide exists — the .NET ecosystem has basically zero antidetect content. I searched for weeks when I first needed this. Every tutorial assumes JavaScript or Python. But if your backend is ASP.NET Core, your infra is Azure, or your team thinks in C#, there's no reason to context-switch just for browser automation.
(Frankly, I think the C# ecosystem gets ignored in the scraping world because most folks assume "enterprise C# devs don't do anything gray-area." Wrong. Plenty of legitimate use cases: ad verification, competitive intel, OSINT.)
Step 1: Create Your Profile in JustBrowser
Before any code runs, create a profile through the JustBrowser desktop app. Each profile stores:
- A fingerprint config: canvas, WebGL, audio, fonts, screen dimensions, timezone, navigator properties, Client Hints
- Optional proxy: HTTP, HTTPS, or SOCKS5 — configured per profile
- Persistent state: cookies, localStorage, IndexedDB, browsing history
- A unique profile ID for API calls
Pick fingerprints that match common hardware. Intel UHD 620/630 for Windows laptops. Apple M2 for macOS. RTX 3060 if you're spoofing a gaming desktop. Don't go exotic — uniqueness is a detection signal. I once configured a profile with some obscure Quadro GPU thinking "more enterprise-looking" and it got flagged faster than a datacenter IP. We covered this in the WebGL fingerprinting deep dive.
Note your profile ID. You'll find it in profile settings or by right-clicking the profile tile in the UI.
Step 2: Set Up Your C# Project
Create a new console app (or add to your existing project):
dotnet new console -n AntidetectAutomation
cd AntidetectAutomation
dotnet add package PuppeteerSharp
Here's the basic structure we'll build out:
using PuppeteerSharp;
using System.Net.Http.Json;
namespace AntidetectAutomation;
public class Program
{
private static readonly HttpClient _http = new();
private const string ApiBase = "http://127.0.0.1:36542/api/v1";
public static async Task Main(string[] args)
{
// Token from the app's API/AI tab — every request needs it
_http.DefaultRequestHeaders.Authorization =
new("Bearer", Environment.GetEnvironmentVariable("JB_API_TOKEN"));
var profileId = "your-profile-id-here";
// Start, connect, automate, cleanup
var cdpUrl = await StartProfileAsync(profileId);
var browser = await ConnectBrowserAsync(cdpUrl);
try
{
await RunAutomationAsync(browser);
}
finally
{
await browser.CloseAsync();
await StopProfileAsync(profileId);
}
}
}
That's the skeleton. Nothing fancy. Now let's fill in the pieces.
Step 3: Start a Profile via REST API
JustBrowser's REST API runs locally when the app is open. Default port: 36542 (configurable in the app), with a Bearer token from the API/AI tab. Hit the start endpoint, get back a CDP WebSocket URL.
public record StartData(string ProfileId, string Status, string CdpUrl, string LaunchedAt);
public record StartResponse(StartData Data);
public static async Task<string> StartProfileAsync(string profileId)
{
var startRequest = new
{
headless = false // only `headless` is accepted here; drop the args field
};
var response = await _http.PostAsJsonAsync(
$"{ApiBase}/profiles/{profileId}/start",
startRequest
);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<StartResponse>();
return result?.Data.CdpUrl
?? throw new InvalidOperationException("No cdp_url returned");
}
What's happening: we POST to the start endpoint with optional config (headed vs headless), and the API returns {"data": {"profile_id", "status": "running", "cdp_url", "launched_at"}}. The cdp_url is a standard CDP endpoint — any CDP-aware tool can connect.
If you're getting connection refused: JustBrowser isn't running. The API only works when the desktop app is open. (Yeah, I've debugged network issues for 20 minutes before realizing the app wasn't launched. More than once.)
Step 4: Connect PuppeteerSharp Over CDP
public static async Task<IBrowser> ConnectBrowserAsync(string cdpUrl)
{
// Give the browser a moment to fully initialize
await Task.Delay(2000);
var browser = await Puppeteer.ConnectAsync(new ConnectOptions
{
BrowserWSEndpoint = cdpUrl,
DefaultViewport = null // Use profile's configured viewport
});
return browser;
}
That's it. Seriously.
PuppeteerSharp connects to the already-running antidetect browser. All your existing code — page.GoToAsync, page.ClickAsync, page.TypeAsync, page.WaitForSelectorAsync — works exactly as before. No API changes, no new abstractions to learn, no wrapper libraries. Just a different browser on the other end of the WebSocket.
The browser happens to report genuine spoofed fingerprints at the engine level. Connecting over the returned cdp_url does not pass --enable-automation, so navigator.webdriver stays false as in a normal Chrome session — no JavaScript override involved.
Step 5: Run Your Automation
public static async Task RunAutomationAsync(IBrowser browser)
{
var pages = await browser.PagesAsync();
var page = pages.FirstOrDefault() ?? await browser.NewPageAsync();
// Verify fingerprint before hitting target
await page.GoToAsync("https://browserleaks.com/canvas");
await Task.Delay(3000);
var canvasHash = await page.EvaluateFunctionAsync<string>(
"() => document.querySelector('.hash')?.textContent"
);
Console.WriteLine($"Canvas fingerprint: {canvasHash}");
// Now hit your actual target
await page.GoToAsync("https://your-target-site.com");
// Your automation logic here
await page.WaitForSelectorAsync("#login-form");
await page.TypeAsync("#email", "[email protected]");
await page.TypeAsync("#password", "correcthorsebatterystaple");
await page.ClickAsync("#submit");
await page.WaitForNavigationAsync();
Console.WriteLine($"Current URL: {page.Url}");
}
Every launch uses the same profile fingerprint. Canvas renders the same hash. WebGL reports the same GPU. Fonts enumerate in the same order.
This matters more than most tutorials acknowledge. Vanilla PuppeteerSharp sessions leak detection signals everywhere. Antidetect profiles give you repeatable, consistent browser identity across runs — which is what platforms expect from real users.
Step 6: Clean Up
public static async Task StopProfileAsync(string profileId)
{
try
{
await _http.PostAsync($"{ApiBase}/profiles/{profileId}/stop", null);
}
catch (HttpRequestException)
{
// Profile might already be stopped — that's fine
}
}
Always stop profiles when done. Each profile is a full Chromium process — leaving them running eats RAM fast. Budget ~500MB per profile, more if you're loading heavy SPAs. I've left a test runner going overnight and come back to 40GB of browser processes. Not great.
Step 7: Scale With a Profile Pool
Running parallel automation? Each worker needs its own profile:
public class ProfilePool
{
private readonly Queue<string> _available;
private readonly Dictionary<string, IBrowser> _active = new();
private readonly SemaphoreSlim _lock = new(1, 1);
private readonly HttpClient _http;
private readonly string _apiBase;
public ProfilePool(IEnumerable<string> profileIds, string apiBase = "http://127.0.0.1:36542/api/v1")
{
_available = new Queue<string>(profileIds);
_apiBase = apiBase;
_http = new HttpClient();
_http.DefaultRequestHeaders.Authorization =
new("Bearer", Environment.GetEnvironmentVariable("JB_API_TOKEN"));
}
public async Task<(string ProfileId, IBrowser Browser)> AcquireAsync()
{
await _lock.WaitAsync();
try
{
if (_available.Count == 0)
throw new InvalidOperationException("No profiles available");
var profileId = _available.Dequeue();
var cdpUrl = await StartProfileInternalAsync(profileId);
await Task.Delay(2000);
var browser = await Puppeteer.ConnectAsync(new ConnectOptions
{
BrowserWSEndpoint = cdpUrl
});
_active[profileId] = browser;
return (profileId, browser);
}
finally
{
_lock.Release();
}
}
public async Task ReleaseAsync(string profileId)
{
await _lock.WaitAsync();
try
{
if (_active.TryGetValue(profileId, out var browser))
{
await browser.CloseAsync();
_active.Remove(profileId);
}
await _http.PostAsync($"{_apiBase}/profiles/{profileId}/stop", null);
_available.Enqueue(profileId);
}
finally
{
_lock.Release();
}
}
private async Task<string> StartProfileInternalAsync(string profileId)
{
var response = await _http.PostAsJsonAsync(
$"{_apiBase}/profiles/{profileId}/start",
new { headless = false }
);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<StartResponse>();
return result!.Data.CdpUrl;
}
}
Now your Parallel.ForEachAsync or Task.WhenAll can acquire profiles from the pool, run automation, release back. Each worker gets isolated fingerprints, cookies, and browsing context.
Pro tip: if you're running 10+ parallel workers on a laptop, watch your memory. Each profile is a full Chromium instance. I've crashed test runs at worker 12 because I didn't budget for 6GB of browser processes. Cloud VMs with 32GB+ are worth the spend for serious parallel work.
Common Errors and How to Fix Them
Error: PuppeteerException: Failed to connect to browser
Cause: The profile didn't finish launching before PuppeteerSharp tried connecting.
Fix: Increase the delay after launch. Or poll the WebSocket port until it responds:
public static async Task WaitForPortAsync(int port, int timeoutSeconds = 30)
{
var deadline = DateTime.UtcNow.AddSeconds(timeoutSeconds);
while (DateTime.UtcNow < deadline)
{
try
{
using var client = new TcpClient();
await client.ConnectAsync("localhost", port);
return;
}
catch (SocketException)
{
await Task.Delay(500);
}
}
throw new TimeoutException($"Port {port} not available after {timeoutSeconds}s");
}
Error: HttpRequestException: Connection refused
Cause: JustBrowser isn't running, or the API port has been changed from the default 36542 in the app's settings.
Fix: Launch JustBrowser. Open the API/AI tab and check the actual API port and token. Also check if something else is bound to 36542 — worth ruling out when you run several local services on the same machine.
Error: Profile launches but fingerprint tests still fail
Cause: You're testing on CreepJS and seeing mismatched values.
Fix: Open the profile manually in JustBrowser, run CreepJS, check if it passes. If it does but fails under PuppeteerSharp, look for conflicting Chrome args. Don't pass --disable-gpu or --headless — let the antidetect browser handle those. The CreepJS testing walkthrough covers common gotchas.
Error: Still getting blocked even with green fingerprint tests
Cause: Detection services score multiple signals. Fingerprint is one. Proxy quality, behavioral patterns, TLS fingerprint are others.
Fix: Check your proxy first. Datacenter IPs get flagged fast — residential is almost mandatory for protected sites. Then check automation speed. Detection services measure time-to-interact, click velocity, scroll patterns. Add randomized delays. Real users don't fill forms in 200ms.
This is frustrating, I know. You fix the fingerprint, CreepJS shows green across the board, and Cloudflare still serves you a challenge page. I've been there. You start doubting everything — is the canvas hash wrong? Is there a new detection vector I don't know about? Nine times out of ten it's the proxy. Residential or bust for anything serious. For email-related automation that needs bulletproof deliverability, JustEmails handles the reputation side.
Next Steps
You've got PuppeteerSharp talking to a fingerprint-protected browser. Where to go from here:
Test before production. Run against BrowserLeaks, CreepJS, and Pixelscan. If these tests show consistent, realistic fingerprints, you're ready. If they show "automation detected" or inconsistent values, something's misconfigured. Don't skip this. I've seen people debug for days what would have been a 5-minute fingerprint check.
Warm up your profiles. Fresh profiles with zero history look suspicious. JustBrowser's cookie warm-up engine visits 50+ common sites to build realistic browsing artifacts. Or automate it yourself — hit Google, YouTube, Wikipedia, let the profile accumulate normal cookies. Profile aging helps long-term — we covered the strategy in the affiliate multi-account playbook.
Match proxies to fingerprints. A profile claiming to be in Chicago shouldn't route through a German datacenter. Geo-match your proxies. Use residential where possible. The ISP proxy setup guide has the details.
Consider the integration pattern. For production workloads, you probably want JustBrowser running as a background service, profiles pre-warmed, and your .NET app connecting on demand. For analytics on scraped data that respects privacy, JustAnalytics provides cookieless tracking that won't leak signals back to platforms you're analyzing. And if you're running ad verification or competitive intelligence, pair with ClickzProtect to detect click fraud in your data sources.
The 7-day trial is enough to validate the whole workflow — unlimited profiles and the REST API are in it from the start, so there's no feature you have to pay to go and check. After that it's $9.99/mo, or $99.99/year. Honestly, the flat price is the part I appreciate: no recalculating what a bigger job costs you every time the profile count doubles.
Frequently Asked Questions
Can PuppeteerSharp connect to an already-running antidetect browser profile?
Yes. JustBrowser's REST API returns a cdp_url when you start a profile. PuppeteerSharp's ConnectAsync method accepts that WebSocket URL directly. You control the browser through PuppeteerSharp while all 40+ fingerprint parameters are handled at the Chromium engine level — no JavaScript patches for a detector to spot.
Why use C# and PuppeteerSharp instead of Node.js Puppeteer for antidetect automation?
If your stack is already .NET — ASP.NET Core backend, Azure Functions, existing C# codebase — switching to Node.js for browser automation adds operational overhead. PuppeteerSharp is a mature .NET port with near-parity to the official Puppeteer API. You get antidetect automation in your existing language without context-switching.
Does the antidetect browser fix the navigator.webdriver flag that PuppeteerSharp triggers?
With JustBrowser you connect to an already-running profile over CDP rather than launching Chrome with --enable-automation, so navigator.webdriver is never set in the first place. Unlike JavaScript overrides that detection services can spot through timing attacks or prototype chain inspection, there is no override in the page at all — the flag reads false, the same as a normal Chrome session.
What happens to cookies and profile state between PuppeteerSharp sessions?
JustBrowser profiles persist cookies, localStorage, IndexedDB, and browsing history across sessions. When you launch a profile via REST API and connect PuppeteerSharp, you're resuming an existing browser state. Close PuppeteerSharp, stop the profile, and those artifacts stay. Next launch picks up where you left off — same fingerprint, same session data.
Try JustBrowser
Native Chromium antidetect browser — not extension-based. Real C++ engine patches at the canvas / WebGL / audio / font / screen layer, so 40+ identity parameters are genuine, not faked. REST API for Playwright, Puppeteer, Selenium. $9.99/month or $99.99/year. 7-day free trial, card required — cancel any time in the seven days and you are not charged. Unlimited profiles.
Related Posts
Ready to manage multiple accounts?
Seven days free, then $9.99/month — one plan, everything included.