JustBrowser
Tutorials13 min read

Bulk-Creating Profiles From Fingerprint Templates: A CSV-to-API Workflow

JustBrowser Platform Team·

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:

  1. Reads fingerprint configurations from a CSV file (one row per profile)
  2. Validates the fingerprint data before sending
  3. Creates profiles via the JustBrowser REST API
  4. 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 requests and pandas installed
  • 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

Ready to manage multiple accounts?

Seven days free, then $9.99/month — one plan, everything included.

We'd like to use Google Analytics, a Google service, to understand how our website is used. It sets two cookies in your browser and runs only if you click Accept. You can change your choice at any time with Cookie settings. Cookie Policy

Sign-in cookies and the cookie that remembers this choice are always on; the website needs them to work.

Google Analytics, a Google service, helps us understand how our website is used. It sets two cookies, _ga and _ga_TVZHQ99TZW. It is now onoff in this browser. If your browser sends a Global Privacy Control or Do Not Track signal, it stays off. Cookie Policy