Bulk-Creating Profiles From Fingerprint Templates: A CSV-to-API Workflow
Picture needing 200 browser profiles. Not twenty. Not fifty. Two hundred — each with a distinct fingerprint, each configured for a different geographic region, each ready to run in a Playwright automation pipeline by Monday morning.
Creating them in the JustBrowser GUI means one profile at a time, clicking through the fingerprint settings for each. Even at a few minutes per profile, 200 profiles adds up to hours of clicking.
Absolutely not.
The REST API exists for this. With a CSV template, a Python script and batch creation, the setup takes longer than the creation itself.
Here's the workflow. By the end, you'll have a repeatable process for bulk profile provisioning that you can run whenever you need to spin up a new set of profiles.
Check your own fingerprint first. We built a public checker at justbrowser.app/fingerprint-checker — 17 rows, client-side, no signup. It grades what it claims rather than guessing: WebRTC candidates are compared against your public IP, and the IP's location is graded on whether its UTC offset actually agrees with your browser's timezone. Worth running before you read the rest of this, so the signals below have your numbers attached to them.
What We're Building
A complete pipeline that:
- Reads fingerprint configurations from a CSV file (one row per profile)
- Validates the fingerprint data before sending
- Creates profiles via the JustBrowser REST API
- Outputs a manifest of created profile IDs for downstream automation
The output: a JSON file mapping your CSV identifiers to JustBrowser profile IDs. Feed that into your Playwright/Puppeteer/Selenium scripts and you're running.
Prerequisites
- JustBrowser ($9.99/mo, or $99.99/year) — the local REST API is part of the plan, and the seven-day trial includes it too
- Python 3.9+ with
requestsandpandasinstalled - JustBrowser running locally — the API listens on 127.0.0.1:36542 by default (configurable in the app)
- An API token from the app's API/AI tab — every request needs
Authorization: Bearer <token> - A CSV file with your fingerprint templates (we'll cover the format)
If you haven't used the API at all yet, start with the Python REST API quickstart. This tutorial assumes you can already hit GET /health and get a response.
Step 1: Design Your Fingerprint Template CSV
Before we write code, we need data. Your CSV should have one row per profile you want to create. Here's a minimal template:
profile_name,timezone,locale,screen_width,screen_height,user_agent
us-east-001,America/New_York,en-US,1920,1080,Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...
us-east-002,America/New_York,en-US,1366,768,Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...
eu-west-001,Europe/London,en-GB,1920,1080,Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36...
That's the basics. But if you're doing this properly — and you are, otherwise why bother with bulk creation — you'll want more parameters:
profile_name,timezone,locale,screen_width,screen_height,user_agent,webgl_vendor,webgl_renderer,canvas_seed,platform,hardware_concurrency,device_memory
us-east-001,America/New_York,en-US,1920,1080,"Mozilla/5.0...",Intel Inc.,Intel Iris OpenGL Engine,0x7a2f3b,Win32,8,16
us-east-002,America/New_York,en-US,1366,768,"Mozilla/5.0...",Google Inc.,ANGLE (Intel, Intel(R) UHD Graphics 620),0x8c4d2e,Win32,4,8
The more fingerprint parameters you control, the more consistent your profiles. JustBrowser handles 40+ parameters internally — but you only need to specify the ones you want to pin. Anything you don't specify gets generated automatically.
(Don't try to specify every parameter. Pin the ones that matter for your operation, let the engine handle the rest.)
A note on WebGL renderer strings: don't invent them. Use real GPU identifiers from actual hardware. An "NVIDIA GeForce RTX 5090" doesn't exist (yet), and detection services maintain hardware databases. Stick to Intel UHD 620/630, NVIDIA GTX 1650/1660, AMD Radeon RX 580 — common consumer GPUs that match your claimed platform. We covered the specifics in the WebGL fingerprinting deep dive.
Step 2: Write the Profile Creation Script
Here's the core script. It reads your CSV, validates each row, creates profiles via API, and outputs a manifest:
import csv
import json
import os
import time
import requests
from pathlib import Path
API_BASE = "http://127.0.0.1:36542/api/v1"
API_TOKEN = os.environ["JUSTBROWSER_TOKEN"] # from the app's API/AI tab
HEADERS = {"Authorization": f"Bearer {API_TOKEN}"} # pass headers=HEADERS to every requests call
def validate_row(row, row_num):
"""Check required fields exist."""
required = ["profile_name", "timezone", "locale"]
missing = [f for f in required if not row.get(f)]
if missing:
print(f"Row {row_num}: Missing fields: {missing}")
return False
return True
def build_fingerprint_config(row):
"""Convert CSV row to JustBrowser fingerprint config."""
config = {
"timezone": row["timezone"],
"locale": row["locale"],
}
# Screen settings
if row.get("screen_width") and row.get("screen_height"):
config["screen"] = {
"width": int(row["screen_width"]),
"height": int(row["screen_height"])
}
# User agent
if row.get("user_agent"):
config["userAgent"] = row["user_agent"]
# WebGL
if row.get("webgl_vendor") and row.get("webgl_renderer"):
config["webgl"] = {
"vendor": row["webgl_vendor"],
"renderer": row["webgl_renderer"]
}
# Canvas noise seed
if row.get("canvas_seed"):
config["canvas"] = {"noiseSeed": row["canvas_seed"]}
# Hardware
if row.get("hardware_concurrency"):
config["hardwareConcurrency"] = int(row["hardware_concurrency"])
if row.get("device_memory"):
config["deviceMemory"] = int(row["device_memory"])
# Platform
if row.get("platform"):
config["platform"] = row["platform"]
return config
def create_profile(name, fingerprint_config):
"""Create a single profile via API."""
payload = {
"name": name,
"fingerprint": fingerprint_config
}
response = requests.post(
f"{API_BASE}/profiles",
json=payload,
headers=HEADERS
)
if response.ok:
return response.json()["data"]["id"]
else:
print(f"Failed to create {name}: {response.text}")
return None
def bulk_create_profiles(csv_path, output_path="profile_manifest.json"):
"""Read CSV, create profiles, output manifest."""
manifest = {}
created = 0
failed = 0
with open(csv_path, "r") as f:
reader = csv.DictReader(f)
rows = list(reader)
print(f"Found {len(rows)} profiles to create")
for i, row in enumerate(rows, 1):
if not validate_row(row, i):
failed += 1
continue
name = row["profile_name"]
config = build_fingerprint_config(row)
profile_id = create_profile(name, config)
if profile_id:
manifest[name] = profile_id
created += 1
print(f"[{i}/{len(rows)}] Created: {name} -> {profile_id}")
else:
failed += 1
# Small delay to avoid hammering the API
time.sleep(0.1)
# Write manifest
with open(output_path, "w") as f:
json.dump(manifest, f, indent=2)
print(f"\nDone. Created: {created}, Failed: {failed}")
print(f"Manifest written to: {output_path}")
return manifest
if __name__ == "__main__":
import sys
csv_file = sys.argv[1] if len(sys.argv) > 1 else "profiles.csv"
bulk_create_profiles(csv_file)
Save this as bulk_create.py. Run it with:
python bulk_create.py my_fingerprints.csv
The output manifest looks like:
{
"us-east-001": "prof_a1b2c3d4e5",
"us-east-002": "prof_f6g7h8i9j0",
"eu-west-001": "prof_k1l2m3n4o5"
}
That manifest is your bridge to automation. Your Playwright/Puppeteer scripts read it, launch the corresponding profile ID, and run.
Step 3: Handling Proxy Configuration
Profiles without proxies are half-baked. You probably want each profile pinned to a specific proxy — residential IP, ISP proxy, whatever matches your use case.
Extend your CSV:
profile_name,timezone,locale,...,proxy_type,proxy_host,proxy_port,proxy_user,proxy_pass
us-east-001,America/New_York,en-US,...,http,us-east.proxy.example.com,8080,user1,pass1
And update the script:
def build_proxy_config(row):
"""Build proxy configuration from CSV row."""
if not row.get("proxy_host"):
return None
proxy = {
"type": row.get("proxy_type", "http"), # http, https, socks5
"host": row["proxy_host"],
"port": int(row.get("proxy_port", 8080))
}
if row.get("proxy_user"):
proxy["username"] = row["proxy_user"]
proxy["password"] = row.get("proxy_pass", "")
return proxy
def create_profile(name, fingerprint_config, proxy_config=None):
"""Create a single profile via API."""
payload = {
"name": name,
"fingerprint": fingerprint_config
}
if proxy_config:
payload["proxy"] = proxy_config
response = requests.post(
f"{API_BASE}/profiles",
json=payload,
headers=HEADERS
)
if response.ok:
return response.json()["data"]["id"]
else:
print(f"Failed to create {name}: {response.text}")
return None
Now each profile gets created with its proxy baked in. When you launch the profile, it routes through that proxy automatically. No runtime configuration needed.
When a proxy dies mid-operation you need to update that profile's config. The API supports PATCH requests for a single profile, and it has a batch proxy endpoint — POST /profiles/batch/proxy — so when your provider rotates 50 IPs you can push the new assignments in one call.
For proxy sourcing guidance — residential vs ISP vs datacenter, and when each makes sense — the ISP proxy setup tutorial has the breakdown.
Step 4: Template Variations for Different Use Cases
One CSV template doesn't fit all scenarios. Here's how to structure templates for different operations:
Multi-region ad verification:
profile_name,timezone,locale,proxy_host,proxy_port,screen_width,screen_height
uk-verify-001,Europe/London,en-GB,uk.residential.proxy,8080,1920,1080
de-verify-001,Europe/Berlin,de-DE,de.residential.proxy,8080,1920,1080
fr-verify-001,Europe/Paris,fr-FR,fr.residential.proxy,8080,1920,1080
E-commerce storefront profiles (one per brand or team member you are authorised to run):
profile_name,timezone,locale,platform,hardware_concurrency,device_memory,user_agent
shop-001,America/Chicago,en-US,Win32,8,16,"Mozilla/5.0 (Windows NT 10.0..."
shop-002,America/Los_Angeles,en-US,MacIntel,8,16,"Mozilla/5.0 (Macintosh..."
shop-003,America/New_York,en-US,Win32,4,8,"Mozilla/5.0 (Windows NT 10.0..."
Social media management:
profile_name,timezone,locale,webgl_vendor,webgl_renderer,canvas_seed
social-client-a-001,America/New_York,en-US,Intel Inc.,Intel Iris OpenGL Engine,0x1a2b3c
social-client-a-002,America/New_York,en-US,Google Inc.,ANGLE (Intel UHD 630),0x4d5e6f
social-client-b-001,Europe/London,en-GB,Intel Inc.,Intel Iris Plus Graphics 655,0x7g8h9i
The key: each template encodes the fingerprint diversity you need for that specific operation. Storefront profiles for different brands or team members might use different hardware settings. Social media management might prioritize geographic consistency per client. Build templates that match your operational requirements.
My opinion, and some people disagree: start with fewer fingerprint parameters, not more. It's tempting to specify everything upfront, but you're creating maintenance burden. If you later need to change your WebGL strategy across 200 profiles, you'll wish you'd let the engine handle it.
Step 5: Validating Bulk-Created Profiles
You've got 200 profiles. Do they actually pass detection tests? Don't assume — verify.
Here's a quick validation script that launches each profile, hits CreepJS, and logs results:
import json
import os
import time
import requests
from playwright.sync_api import sync_playwright
API_BASE = "http://127.0.0.1:36542/api/v1"
API_TOKEN = os.environ["JUSTBROWSER_TOKEN"]
HEADERS = {"Authorization": f"Bearer {API_TOKEN}"}
def validate_profile(profile_id, profile_name):
"""Start profile, hit fingerprint test, check for consistency."""
# Start
response = requests.post(
f"{API_BASE}/profiles/{profile_id}/start",
json={"headless": False},
headers=HEADERS
)
if not response.ok:
return {"name": profile_name, "status": "launch_failed"}
# The start response carries the CDP endpoint to attach to
cdp_url = response.json()["data"]["cdp_url"]
time.sleep(3) # Wait for browser init
# Navigate to CreepJS over CDP
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(cdp_url)
context = browser.contexts[0]
page = context.pages[0] if context.pages else context.new_page()
page.goto("https://abrahamjuliot.github.io/creepjs/")
time.sleep(5) # Let CreepJS run
# Stop profile
requests.post(f"{API_BASE}/profiles/{profile_id}/stop", headers=HEADERS)
return {"name": profile_name, "status": "checked"}
def validate_manifest(manifest_path):
"""Validate all profiles in a manifest."""
with open(manifest_path) as f:
manifest = json.load(f)
results = []
for name, profile_id in manifest.items():
print(f"Validating {name}...")
result = validate_profile(profile_id, name)
results.append(result)
time.sleep(1) # Breathing room between profiles
return results
This is a spot-check, not a full audit. For production validation, you'd parse the CreepJS results programmatically — but that's a whole other tutorial. (And honestly, parsing CreepJS output is its own special kind of headache. The DOM structure changes occasionally.) The CreepJS testing walkthrough covers the full approach.
Common Errors and How to Fix Them
"Profile name already exists" on creation
Cause: You're rerunning the script without cleaning up previous profiles.
Fix: Either use unique names (append timestamps, UUIDs) or delete existing profiles first:
# Add to your script
def delete_profile_if_exists(name):
response = requests.get(f"{API_BASE}/profiles", headers=HEADERS)
profiles = response.json()["data"]
for p in profiles:
if p["name"] == name:
requests.delete(f"{API_BASE}/profiles/{p['id']}", headers=HEADERS)
return True
return False
Invalid fingerprint configuration errors
Cause: Your CSV has malformed data — wrong types, invalid values, contradictory settings.
Fix: Validate before sending. Common issues:
- Screen dimensions as strings instead of integers
- User agent missing platform-matching values
- WebGL renderer that doesn't match the claimed platform (e.g., Apple GPU on Windows user agent)
The fingerprint consistency problem is real. A profile claiming Windows 10 but reporting an Apple M2 GPU will fail instantly. Match your values. We covered the logic in the audio/font/hardware fingerprinting guide.
Errors when creating profiles too fast
Cause: Hammering the local API in a tight loop. There's no published rate limit, but a burst of writes can still trip over itself.
Fix: Add a time.sleep(0.1) or time.sleep(0.2) between API calls. The script above includes this. If you still see failures, back off to 0.5 seconds, or switch to POST /profiles/bulk for the batch. For 500 profiles, the delay costs you ~4 minutes total. Not elegant. But it works every time.
Manifest file gets overwritten
Cause: You ran the script twice with different CSVs but same output file.
Fix: Use unique manifest names. Pass the output path as an argument:
bulk_create_profiles("campaign_a.csv", "manifest_campaign_a.json")
bulk_create_profiles("campaign_b.csv", "manifest_campaign_b.json")
Next Steps
You've got bulk profile creation working. Here's where to go from here:
Integrate with your automation framework. The manifest file maps names to profile IDs. Load it in your Playwright/Puppeteer script, iterate through profiles, launch each one for your automation task. The Playwright/Puppeteer integration guide shows the connection pattern.
Set up profile warming. Fresh profiles with zero browsing history can trigger extra checks. JustBrowser includes a cookie warm-up engine that hits 127 curated sites — configure that for new profiles before using them for important work.
Build template generators. Instead of hand-crafting CSVs, write scripts that generate fingerprint templates based on target demographics. Need 100 US Windows users? Generate a CSV with appropriate timezones, locales, and hardware distributions. Need 50 German Mac users? Different template.
Track profile health over time. Profiles age. Some get flagged. Build monitoring that periodically checks each profile against detection services and flags any that start failing. Profiles can pass CreepJS for months, then suddenly fail after a detection service update. It happens. The profile maintenance checklist covers the operational discipline.
If you're running ad campaigns alongside your automation, ClickzProtect can help catch fraudulent traffic on the ad side. And for analytics on campaign performance without leaking data back to platforms, JustAnalytics provides privacy-first tracking.
The seven-day trial is full access, REST API included — enough to run this whole workflow end to end before the first charge lands. After that it's $9.99/mo, or $99.99/year, for unlimited profiles. There's no bulk-operations tier above it to upgrade into.
FAQ
Can I create antidetect profiles in bulk without using the GUI?
Yes. JustBrowser's REST API includes a profile creation endpoint that accepts fingerprint configurations as JSON. You can script profile creation from any language — Python, Node, Go, even bash with curl. The GUI is optional for profile management — the API ships in the same single plan, so there's nothing to unlock first.
What fingerprint parameters should my CSV template include?
At minimum: screen resolution, timezone, locale, and user agent. For better detection resistance, include WebGL vendor/renderer strings, canvas noise seed, audio context properties, and font lists. JustBrowser handles 40+ parameters — the more you specify in templates, the more consistent your profiles will be across bulk creation.
How many profiles can I create per API call?
The single-profile endpoint takes one profile per request, so you can loop through your CSV rows and POST each one. The local API has no published rate limit; keep a small delay between calls and use POST /profiles/bulk for large batches.
Do bulk-created profiles get the same fingerprint quality as GUI-created ones?
Identical. The API and GUI both feed into the same profile storage and fingerprint engine. The only difference is input method. API-created profiles use whatever fingerprint values you supply; GUI-created profiles use JustBrowser's built-in fingerprint generator. If you supply valid fingerprint data via API, you get the same native C++ spoofing as any other profile.
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.
Get started → · How it differs from Multilogin / GoLogin / AdsPower
Related Posts
Ready to manage multiple accounts?
Seven days free, then $9.99/month — one plan, everything included.